0d25548ed5
- Move utility scripts to scripts/ (check_market, check_positions, etc.) - Move test files to tests/ (test_modules, test_mt5_connection, etc.) - Move deprecated dashboards to archive/ - Move research files to docs/research/ - Add sys.path fix to all moved Python files - Rewrite README.md with architecture diagram and badges - Add CLAUDE.md project guide - Add MIT LICENSE - Update .gitignore with archive/ pattern Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1148 lines
43 KiB
Python
1148 lines
43 KiB
Python
"""
|
|
Trading Bot Dashboard PRO - MONITORING ONLY
|
|
============================================
|
|
Pure monitoring dashboard (no control) with:
|
|
- Real-time Price & Equity charts
|
|
- Visual alarms for critical conditions
|
|
- Data freshness/heartbeat indicator
|
|
- Detailed AI reasoning in logs
|
|
- Stale data visual warning
|
|
"""
|
|
|
|
import customtkinter as ctk
|
|
import threading
|
|
import time
|
|
from datetime import datetime
|
|
from zoneinfo import ZoneInfo
|
|
from pathlib import Path
|
|
from collections import deque
|
|
import sys
|
|
|
|
# Add project to path
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
|
|
# Load environment variables
|
|
from dotenv import load_dotenv
|
|
load_dotenv()
|
|
|
|
# Matplotlib for charts
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|
from matplotlib.figure import Figure
|
|
import matplotlib
|
|
matplotlib.use('TkAgg')
|
|
|
|
# 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
|
|
from loguru import logger
|
|
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",
|
|
"yellow": "#ffd93d",
|
|
"highlight": "#2d4a2d",
|
|
"highlight_off": "#2d2d44",
|
|
"chart_bg": "#0d0d1a",
|
|
"chart_line": "#00d4ff",
|
|
"chart_equity": "#00ff88",
|
|
"danger": "#ff2222",
|
|
"stale": "#444444", # Gray for stale data
|
|
},
|
|
"light": {
|
|
"bg": "#f0f2f5",
|
|
"card": "#ffffff",
|
|
"card_inner": "#f8f9fa",
|
|
"text": "#1a1a2e",
|
|
"text_secondary": "#555555",
|
|
"text_muted": "#888888",
|
|
"accent": "#0066cc",
|
|
"green": "#00aa55",
|
|
"red": "#dd3344",
|
|
"orange": "#ee8800",
|
|
"yellow": "#cc9900",
|
|
"highlight": "#d4edda",
|
|
"highlight_off": "#e9ecef",
|
|
"chart_bg": "#f8f9fa",
|
|
"chart_line": "#0066cc",
|
|
"chart_equity": "#00aa55",
|
|
"danger": "#cc0000",
|
|
"stale": "#cccccc",
|
|
}
|
|
}
|
|
|
|
|
|
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"])
|
|
|
|
def set_alarm(self, is_alarm=False):
|
|
"""Set alarm state (red background)"""
|
|
colors = THEMES[self.theme]
|
|
if is_alarm:
|
|
self.configure(fg_color=colors["danger"])
|
|
else:
|
|
self.configure(fg_color=colors["card"])
|
|
|
|
|
|
class TradingDashboardPro(ctk.CTk):
|
|
"""Production Monitoring Dashboard - NO CONTROL, PURE MONITORING"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
# Theme state
|
|
self.current_theme = "dark"
|
|
ctk.set_appearance_mode("dark")
|
|
|
|
# Window setup
|
|
self.title("AI Trading Bot - Monitor")
|
|
self.geometry("750x900")
|
|
self.minsize(650, 750)
|
|
self.configure(fg_color=THEMES[self.current_theme]["bg"])
|
|
|
|
# Store all 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
|
|
self.last_data_time = None # For stale data detection
|
|
self.data_stale = False
|
|
self.consecutive_errors = 0
|
|
|
|
# Data history for charts
|
|
self.price_history = deque(maxlen=120) # 2 hours of data
|
|
self.equity_history = deque(maxlen=120)
|
|
self.balance_history = deque(maxlen=120)
|
|
self.time_history = deque(maxlen=120)
|
|
|
|
# Risk alarm state
|
|
self.risk_alarm_active = False
|
|
|
|
# Create UI
|
|
self.create_header()
|
|
self.create_main_layout()
|
|
self.create_status_bar()
|
|
|
|
# Start connection
|
|
self.after(100, self.connect_systems)
|
|
|
|
# Heartbeat checker
|
|
self.after(5000, self.check_data_freshness)
|
|
|
|
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["accent"])
|
|
self.time_label.configure(text_color=colors["text_secondary"])
|
|
|
|
# Update charts
|
|
self.update_price_chart()
|
|
self.update_equity_chart()
|
|
|
|
# Update text boxes
|
|
if hasattr(self, 'log_text'):
|
|
self.log_text.configure(fg_color=colors["card_inner"], text_color=colors["green"])
|
|
|
|
def check_data_freshness(self):
|
|
"""Check if data is stale (>5 seconds old) - HEARTBEAT"""
|
|
colors = THEMES[self.current_theme]
|
|
|
|
if self.last_data_time:
|
|
age = (datetime.now() - self.last_data_time).total_seconds()
|
|
|
|
if age > 5:
|
|
# DATA IS STALE - Visual warning
|
|
self.data_stale = True
|
|
self.heartbeat_label.configure(
|
|
text=f"DATA STALE ({age:.0f}s)",
|
|
text_color=colors["red"]
|
|
)
|
|
# Gray out the main window slightly
|
|
self.status_frame.configure(fg_color=colors["stale"])
|
|
self.status_label.configure(text="WARNING: Data tidak terupdate!", text_color=colors["red"])
|
|
else:
|
|
self.data_stale = False
|
|
self.heartbeat_label.configure(
|
|
text=f"LIVE ({age:.1f}s)",
|
|
text_color=colors["green"]
|
|
)
|
|
if self.consecutive_errors == 0:
|
|
self.status_frame.configure(fg_color=colors["card"])
|
|
|
|
# Schedule next check
|
|
self.after(1000, self.check_data_freshness)
|
|
|
|
def create_header(self):
|
|
"""Create header section with heartbeat indicator"""
|
|
colors = THEMES[self.current_theme]
|
|
|
|
header_frame = ctk.CTkFrame(self, fg_color="transparent", height=45)
|
|
header_frame.pack(fill="x", padx=15, pady=(8, 3))
|
|
header_frame.pack_propagate(False)
|
|
|
|
# Logo/Title
|
|
self.title_label = ctk.CTkLabel(
|
|
header_frame,
|
|
text="AI TRADING BOT",
|
|
font=ctk.CTkFont(size=18, weight="bold"),
|
|
text_color=colors["text"]
|
|
)
|
|
self.title_label.pack(side="left")
|
|
|
|
self.subtitle_label = ctk.CTkLabel(
|
|
header_frame,
|
|
text=" MONITOR",
|
|
font=ctk.CTkFont(size=11),
|
|
text_color=colors["accent"]
|
|
)
|
|
self.subtitle_label.pack(side="left", pady=(3, 0))
|
|
|
|
# Theme toggle
|
|
self.theme_btn = ctk.CTkButton(
|
|
header_frame,
|
|
text="Light",
|
|
width=55,
|
|
height=24,
|
|
corner_radius=12,
|
|
font=ctk.CTkFont(size=10),
|
|
command=self.toggle_theme
|
|
)
|
|
self.theme_btn.pack(side="right", padx=3)
|
|
|
|
# Heartbeat indicator (DATA FRESHNESS)
|
|
self.heartbeat_label = ctk.CTkLabel(
|
|
header_frame,
|
|
text="CONNECTING...",
|
|
font=ctk.CTkFont(size=10, weight="bold"),
|
|
text_color=colors["orange"]
|
|
)
|
|
self.heartbeat_label.pack(side="right", padx=10)
|
|
|
|
# Connection status
|
|
self.connection_label = ctk.CTkLabel(
|
|
header_frame,
|
|
text="Connecting...",
|
|
font=ctk.CTkFont(size=10),
|
|
text_color=colors["orange"]
|
|
)
|
|
self.connection_label.pack(side="right", padx=8)
|
|
|
|
# Time display
|
|
self.time_label = ctk.CTkLabel(
|
|
header_frame,
|
|
text="--:--:-- WIB",
|
|
font=ctk.CTkFont(size=11, weight="bold"),
|
|
text_color=colors["text_secondary"]
|
|
)
|
|
self.time_label.pack(side="right", padx=8)
|
|
|
|
def create_main_layout(self):
|
|
"""Create main dashboard layout"""
|
|
# Scrollable main container
|
|
self.main_scroll = ctk.CTkScrollableFrame(self, fg_color="transparent")
|
|
self.main_scroll.pack(fill="both", expand=True, padx=10, pady=3)
|
|
|
|
# Configure 2-column grid
|
|
self.main_scroll.grid_columnconfigure(0, weight=1, minsize=300)
|
|
self.main_scroll.grid_columnconfigure(1, weight=1, minsize=300)
|
|
|
|
# Row 0: Price Chart (full width)
|
|
self.create_price_chart_card(self.main_scroll, 0, 0)
|
|
|
|
# Row 1: Price & Account
|
|
self.create_price_card(self.main_scroll, 1, 0)
|
|
self.create_account_card(self.main_scroll, 1, 1)
|
|
|
|
# Row 2: Session & Risk (with ALARM capability)
|
|
self.create_session_card(self.main_scroll, 2, 0)
|
|
self.create_risk_card(self.main_scroll, 2, 1)
|
|
|
|
# Row 3: SMC & ML
|
|
self.create_smc_card(self.main_scroll, 3, 0)
|
|
self.create_ml_card(self.main_scroll, 3, 1)
|
|
|
|
# Row 4: Regime & Positions
|
|
self.create_regime_card(self.main_scroll, 4, 0)
|
|
self.create_positions_card(self.main_scroll, 4, 1)
|
|
|
|
# Row 5: Equity Chart (replaced Settings) - full width
|
|
self.create_equity_chart_card(self.main_scroll, 5, 0)
|
|
|
|
# Row 6: Log (full width, larger)
|
|
self.create_log_card(self.main_scroll, 6, 0)
|
|
|
|
def create_price_chart_card(self, parent, row, col):
|
|
"""Mini price chart - sparkline style"""
|
|
colors = THEMES[self.current_theme]
|
|
card = ModernCard(parent, "PRICE CHART (2H)", theme=self.current_theme)
|
|
card.grid(row=row, column=col, columnspan=2, padx=4, pady=4, sticky="nsew")
|
|
self.cards.append(card)
|
|
|
|
# Create matplotlib figure
|
|
self.price_fig = Figure(figsize=(6, 1.3), dpi=100, facecolor=colors["chart_bg"])
|
|
self.price_ax = self.price_fig.add_subplot(111)
|
|
self.price_ax.set_facecolor(colors["chart_bg"])
|
|
|
|
# Style
|
|
self.price_ax.tick_params(colors=colors["text_muted"], labelsize=7)
|
|
for spine in ['top', 'right']:
|
|
self.price_ax.spines[spine].set_visible(False)
|
|
for spine in ['bottom', 'left']:
|
|
self.price_ax.spines[spine].set_color(colors["text_muted"])
|
|
|
|
self.price_fig.tight_layout(pad=0.3)
|
|
|
|
# Embed in tkinter
|
|
self.price_canvas = FigureCanvasTkAgg(self.price_fig, master=card.content)
|
|
self.price_canvas.get_tk_widget().pack(fill="both", expand=True)
|
|
|
|
def create_equity_chart_card(self, parent, row, col):
|
|
"""Equity/Balance chart - REPLACED SETTINGS"""
|
|
colors = THEMES[self.current_theme]
|
|
card = ModernCard(parent, "EQUITY vs BALANCE (2H)", theme=self.current_theme)
|
|
card.grid(row=row, column=col, columnspan=2, padx=4, pady=4, sticky="nsew")
|
|
self.cards.append(card)
|
|
|
|
# Create matplotlib figure
|
|
self.equity_fig = Figure(figsize=(6, 1.3), dpi=100, facecolor=colors["chart_bg"])
|
|
self.equity_ax = self.equity_fig.add_subplot(111)
|
|
self.equity_ax.set_facecolor(colors["chart_bg"])
|
|
|
|
# Style
|
|
self.equity_ax.tick_params(colors=colors["text_muted"], labelsize=7)
|
|
for spine in ['top', 'right']:
|
|
self.equity_ax.spines[spine].set_visible(False)
|
|
for spine in ['bottom', 'left']:
|
|
self.equity_ax.spines[spine].set_color(colors["text_muted"])
|
|
|
|
self.equity_fig.tight_layout(pad=0.3)
|
|
|
|
# Embed
|
|
self.equity_canvas = FigureCanvasTkAgg(self.equity_fig, master=card.content)
|
|
self.equity_canvas.get_tk_widget().pack(fill="both", expand=True)
|
|
|
|
def update_price_chart(self):
|
|
"""Redraw the price chart"""
|
|
if len(self.price_history) < 2:
|
|
return
|
|
|
|
colors = THEMES[self.current_theme]
|
|
self.price_ax.clear()
|
|
|
|
prices = list(self.price_history)
|
|
self.price_ax.plot(prices, color=colors["chart_line"], linewidth=1.5)
|
|
self.price_ax.fill_between(range(len(prices)), prices, alpha=0.15, color=colors["chart_line"])
|
|
|
|
# Style
|
|
self.price_ax.set_facecolor(colors["chart_bg"])
|
|
self.price_ax.tick_params(colors=colors["text_muted"], labelsize=7)
|
|
for spine in ['top', 'right']:
|
|
self.price_ax.spines[spine].set_visible(False)
|
|
for spine in ['bottom', 'left']:
|
|
self.price_ax.spines[spine].set_color(colors["text_muted"])
|
|
|
|
self.price_ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'{x:.0f}'))
|
|
self.price_fig.tight_layout(pad=0.3)
|
|
self.price_canvas.draw()
|
|
|
|
def update_equity_chart(self):
|
|
"""Redraw equity/balance chart"""
|
|
if len(self.equity_history) < 2:
|
|
return
|
|
|
|
colors = THEMES[self.current_theme]
|
|
self.equity_ax.clear()
|
|
|
|
equity = list(self.equity_history)
|
|
balance = list(self.balance_history)
|
|
|
|
# Plot both lines
|
|
self.equity_ax.plot(equity, color=colors["chart_equity"], linewidth=1.5, label='Equity')
|
|
self.equity_ax.plot(balance, color=colors["text_muted"], linewidth=1, linestyle='--', label='Balance')
|
|
|
|
# Fill between (profit/loss visual)
|
|
self.equity_ax.fill_between(
|
|
range(len(equity)), balance, equity,
|
|
where=[e >= b for e, b in zip(equity, balance)],
|
|
alpha=0.2, color=colors["green"]
|
|
)
|
|
self.equity_ax.fill_between(
|
|
range(len(equity)), balance, equity,
|
|
where=[e < b for e, b in zip(equity, balance)],
|
|
alpha=0.2, color=colors["red"]
|
|
)
|
|
|
|
# Style
|
|
self.equity_ax.set_facecolor(colors["chart_bg"])
|
|
self.equity_ax.tick_params(colors=colors["text_muted"], labelsize=7)
|
|
for spine in ['top', 'right']:
|
|
self.equity_ax.spines[spine].set_visible(False)
|
|
for spine in ['bottom', 'left']:
|
|
self.equity_ax.spines[spine].set_color(colors["text_muted"])
|
|
|
|
self.equity_ax.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x:.0f}'))
|
|
self.equity_fig.tight_layout(pad=0.3)
|
|
self.equity_canvas.draw()
|
|
|
|
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=26, weight="bold"),
|
|
text_color=colors["green"]
|
|
)
|
|
self.price_label.pack(pady=(3, 0))
|
|
|
|
# Change indicator
|
|
self.price_change_label = ctk.CTkLabel(
|
|
card.content,
|
|
text="-- (--)",
|
|
font=ctk.CTkFont(size=10),
|
|
text_color=colors["text_muted"]
|
|
)
|
|
self.price_change_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=(2, 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=10), text_color=colors["text_secondary"]).pack(side="left")
|
|
self.balance_label = ctk.CTkLabel(balance_frame, text="$-----.--", font=ctk.CTkFont(size=10, 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=10), text_color=colors["text_secondary"]).pack(side="left")
|
|
self.equity_label = ctk.CTkLabel(equity_frame, text="$-----.--", font=ctk.CTkFont(size=10, 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=10), text_color=colors["text_secondary"]).pack(side="left")
|
|
self.pl_label = ctk.CTkLabel(pl_frame, text="$0.00", font=ctk.CTkFont(size=11, 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=11, weight="bold"),
|
|
text_color=colors["orange"]
|
|
)
|
|
self.session_label.pack(pady=(2, 0))
|
|
|
|
# Golden time
|
|
self.golden_frame = ctk.CTkFrame(card.content, fg_color=colors["highlight_off"], corner_radius=6)
|
|
self.golden_frame.pack(pady=5, 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=3)
|
|
|
|
# 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 - WITH ALARM CAPABILITY"""
|
|
colors = THEMES[self.current_theme]
|
|
self.risk_card = ModernCard(parent, "RISK STATUS", theme=self.current_theme)
|
|
self.risk_card.grid(row=row, column=col, padx=4, pady=4, sticky="nsew")
|
|
self.cards.append(self.risk_card)
|
|
|
|
# Daily loss
|
|
dl_frame = ctk.CTkFrame(self.risk_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(self.risk_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(self.risk_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")
|
|
|
|
# Risk % indicator
|
|
risk_pct_frame = ctk.CTkFrame(self.risk_card.content, fg_color="transparent")
|
|
risk_pct_frame.pack(fill="x", pady=2)
|
|
ctk.CTkLabel(risk_pct_frame, text="Risk Used:", font=ctk.CTkFont(size=10), text_color=colors["text_secondary"]).pack(side="left")
|
|
self.risk_pct_label = ctk.CTkLabel(risk_pct_frame, text="0%", font=ctk.CTkFont(size=10, weight="bold"), text_color=colors["green"])
|
|
self.risk_pct_label.pack(side="right")
|
|
|
|
def create_smc_card(self, parent, row, col):
|
|
"""SMC Signal card with reasoning"""
|
|
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=22, weight="bold"),
|
|
text_color=colors["text_muted"]
|
|
)
|
|
self.smc_signal_label.pack(pady=(3, 2))
|
|
|
|
# Confidence bar
|
|
self.smc_confidence_bar = ctk.CTkProgressBar(card.content, height=6, corner_radius=3)
|
|
self.smc_confidence_bar.pack(pady=3, fill="x", padx=10)
|
|
self.smc_confidence_bar.set(0)
|
|
|
|
# Reason (AI REASONING)
|
|
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=(2, 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=22, weight="bold"),
|
|
text_color=colors["text_muted"]
|
|
)
|
|
self.ml_signal_label.pack(pady=(3, 2))
|
|
|
|
# Confidence bar
|
|
self.ml_confidence_bar = ctk.CTkProgressBar(card.content, height=6, corner_radius=3)
|
|
self.ml_confidence_bar.pack(pady=3, fill="x", padx=10)
|
|
self.ml_confidence_bar.set(0)
|
|
|
|
# Probabilities
|
|
self.ml_prob_label = ctk.CTkLabel(
|
|
card.content,
|
|
text="Buy: --% | Sell: --%",
|
|
font=ctk.CTkFont(size=10),
|
|
text_color=colors["text_secondary"]
|
|
)
|
|
self.ml_prob_label.pack()
|
|
|
|
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=14, weight="bold"),
|
|
text_color=colors["text_muted"]
|
|
)
|
|
self.regime_label.pack(pady=(5, 3))
|
|
|
|
# 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 frame
|
|
self.positions_frame = ctk.CTkScrollableFrame(
|
|
card.content,
|
|
fg_color=colors["card_inner"],
|
|
height=70,
|
|
corner_radius=6
|
|
)
|
|
self.positions_frame.pack(fill="both", expand=True)
|
|
|
|
# Placeholder
|
|
self.no_positions_label = ctk.CTkLabel(
|
|
self.positions_frame,
|
|
text="No open positions",
|
|
font=ctk.CTkFont(size=10),
|
|
text_color=colors["text_muted"]
|
|
)
|
|
self.no_positions_label.pack(pady=10)
|
|
|
|
def create_log_card(self, parent, row, col):
|
|
"""Activity log card - LARGER for better monitoring"""
|
|
colors = THEMES[self.current_theme]
|
|
card = ModernCard(parent, "AI 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=120, # Larger log area
|
|
corner_radius=6
|
|
)
|
|
self.log_text.pack(fill="both", expand=True)
|
|
|
|
def create_status_bar(self):
|
|
"""Create status bar with data freshness indicator"""
|
|
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, level="info"):
|
|
"""Add message to activity log with better formatting"""
|
|
try:
|
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
colors = THEMES[self.current_theme]
|
|
|
|
# Color based on level
|
|
if level == "error":
|
|
prefix = "[ERROR]"
|
|
elif level == "warn":
|
|
prefix = "[WARN]"
|
|
elif level == "trade":
|
|
prefix = "[TRADE]"
|
|
else:
|
|
prefix = "[INFO]"
|
|
|
|
self.log_text.insert("end", f"[{timestamp}] {prefix} {message}\n")
|
|
self.log_text.see("end")
|
|
|
|
# Also log to file
|
|
logger.info(f"Dashboard: {message}")
|
|
except Exception as e:
|
|
print(f"Log error: {e}")
|
|
|
|
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("Failed to connect to MT5!", "error")
|
|
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"Connection error: {e}", "error")
|
|
logger.error(f"Dashboard connect error: {e}")
|
|
self.connection_label.configure(text="Error", text_color=colors["red"])
|
|
|
|
def update_loop(self):
|
|
"""Main update loop with proper error tracking"""
|
|
while self.running:
|
|
try:
|
|
self.after(0, self.update_data)
|
|
self.consecutive_errors = 0
|
|
time.sleep(1)
|
|
except Exception as e:
|
|
self.consecutive_errors += 1
|
|
logger.error(f"Dashboard update error: {e}")
|
|
self.after(0, lambda: self.log(f"Update error: {e}", "error"))
|
|
time.sleep(3)
|
|
|
|
def update_data(self):
|
|
"""Fetch and update all data"""
|
|
if not self.mt5:
|
|
return
|
|
|
|
colors = THEMES[self.current_theme]
|
|
|
|
# Mark data as fresh
|
|
self.last_data_time = datetime.now()
|
|
|
|
# Update time
|
|
wib = ZoneInfo("Asia/Jakarta")
|
|
now = datetime.now(wib)
|
|
self.time_label.configure(text=now.strftime("%H:%M:%S WIB"))
|
|
|
|
# Check golden time
|
|
is_golden = 19 <= now.hour < 23
|
|
if is_golden:
|
|
self.golden_frame.configure(fg_color=colors["highlight"])
|
|
self.golden_label.configure(text="GOLDEN: YES", text_color=colors["green"])
|
|
else:
|
|
self.golden_frame.configure(fg_color=colors["highlight_off"])
|
|
self.golden_label.configure(text="GOLDEN: 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")
|
|
|
|
# Update price history
|
|
self.price_history.append(price)
|
|
|
|
# Calculate change
|
|
if len(self.price_history) > 1:
|
|
prev_price = list(self.price_history)[-2]
|
|
change = price - prev_price
|
|
if change >= 0:
|
|
self.price_change_label.configure(text=f"+{change:.2f}", text_color=colors["green"])
|
|
else:
|
|
self.price_change_label.configure(text=f"{change:.2f}", text_color=colors["red"])
|
|
|
|
# Update chart every 10 ticks
|
|
if len(self.price_history) % 10 == 0:
|
|
self.update_price_chart()
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Price update error: {e}")
|
|
|
|
# Update account & equity chart
|
|
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"])
|
|
|
|
# Update equity/balance history
|
|
self.equity_history.append(equity)
|
|
self.balance_history.append(balance)
|
|
|
|
# Update equity chart every 10 updates
|
|
if len(self.equity_history) % 10 == 0:
|
|
self.update_equity_chart()
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Account update error: {e}")
|
|
|
|
# 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 Exception as e:
|
|
logger.debug(f"Session update error: {e}")
|
|
|
|
# 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 Exception as e:
|
|
logger.debug(f"Signal update error: {e}")
|
|
|
|
# Update positions
|
|
try:
|
|
positions = self.mt5.get_open_positions(self.config.symbol)
|
|
self.update_positions(positions)
|
|
except Exception as e:
|
|
logger.debug(f"Position update error: {e}")
|
|
|
|
# Update risk state with ALARM check
|
|
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')}")
|
|
|
|
if not self.data_stale:
|
|
self.status_label.configure(text="Monitoring...")
|
|
|
|
def update_signals(self, df):
|
|
"""Update SMC, ML, and Regime signals with REASONING"""
|
|
colors = THEMES[self.current_theme]
|
|
|
|
try:
|
|
df = self.feature_eng.calculate_all(df, include_ml_features=True)
|
|
df = self.smc.calculate_all(df)
|
|
except Exception as e:
|
|
logger.debug(f"Feature calculation error: {e}")
|
|
return
|
|
|
|
# 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 Exception as e:
|
|
logger.debug(f"Regime update error: {e}")
|
|
|
|
# SMC Signal with REASONING
|
|
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)
|
|
|
|
# Show AI REASONING
|
|
reason = smc_signal.reason if smc_signal.reason else "Signal generated"
|
|
self.smc_reason_label.configure(text=reason[:80] + "..." if len(reason) > 80 else 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_reason_label.configure(text="Waiting for SMC setup...")
|
|
except Exception as e:
|
|
logger.debug(f"SMC update error: {e}")
|
|
|
|
# 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)
|
|
|
|
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 Exception as e:
|
|
logger.debug(f"ML update error: {e}")
|
|
|
|
def update_positions(self, positions):
|
|
"""Update positions display"""
|
|
colors = THEMES[self.current_theme]
|
|
|
|
# Clear existing
|
|
for widget in self.positions_frame.winfo_children():
|
|
widget.destroy()
|
|
|
|
if positions is None or positions.is_empty():
|
|
label = ctk.CTkLabel(
|
|
self.positions_frame,
|
|
text="No open positions",
|
|
font=ctk.CTkFont(size=10),
|
|
text_color=colors["text_muted"]
|
|
)
|
|
label.pack(pady=10)
|
|
return
|
|
|
|
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)
|
|
|
|
# Position row
|
|
pos_frame = ctk.CTkFrame(self.positions_frame, fg_color="transparent")
|
|
pos_frame.pack(fill="x", pady=1, padx=3)
|
|
|
|
type_color = colors["green"] if pos_type == "BUY" else colors["red"]
|
|
profit_color = colors["green"] if profit >= 0 else colors["red"]
|
|
|
|
ctk.CTkLabel(
|
|
pos_frame,
|
|
text=f"{pos_type} {volume} @ {price_open:.2f}",
|
|
font=ctk.CTkFont(size=9, weight="bold"),
|
|
text_color=type_color
|
|
).pack(side="left")
|
|
|
|
ctk.CTkLabel(
|
|
pos_frame,
|
|
text=f"${profit:+.2f}",
|
|
font=ctk.CTkFont(size=9, weight="bold"),
|
|
text_color=profit_color
|
|
).pack(side="right")
|
|
|
|
def update_risk_state(self):
|
|
"""Update risk state with ALARM for high risk"""
|
|
colors = THEMES[self.current_theme]
|
|
|
|
try:
|
|
risk_file = Path("data/risk_state.txt")
|
|
if risk_file.exists():
|
|
content = risk_file.read_text()
|
|
lines = content.strip().split('\n')
|
|
|
|
daily_loss = 0.0
|
|
daily_profit = 0.0
|
|
consec_losses = 0
|
|
|
|
for line in lines:
|
|
if ':' in line:
|
|
key, value = line.split(':', 1)
|
|
key = key.strip()
|
|
value = value.strip()
|
|
|
|
if key == 'daily_loss':
|
|
daily_loss = float(value)
|
|
self.daily_loss_label.configure(text=f"${daily_loss:,.2f}")
|
|
elif key == 'daily_profit':
|
|
daily_profit = float(value)
|
|
self.daily_profit_label.configure(text=f"${daily_profit:,.2f}")
|
|
elif key == 'consecutive_losses':
|
|
consec_losses = int(value)
|
|
self.consec_loss_label.configure(text=str(consec_losses))
|
|
|
|
# Calculate risk percentage (based on $250 max daily loss = 5% of $5000)
|
|
max_daily_loss = self.config.capital * (self.config.risk.max_daily_loss / 100)
|
|
risk_pct = (daily_loss / max_daily_loss * 100) if max_daily_loss > 0 else 0
|
|
|
|
self.risk_pct_label.configure(text=f"{risk_pct:.0f}%")
|
|
|
|
# ALARM: If risk > 80%
|
|
if risk_pct >= 80:
|
|
self.risk_card.set_alarm(True)
|
|
self.risk_pct_label.configure(text_color=colors["danger"])
|
|
if not self.risk_alarm_active:
|
|
self.log(f"RISK ALARM: Daily loss at {risk_pct:.0f}%!", "warn")
|
|
self.risk_alarm_active = True
|
|
elif risk_pct >= 50:
|
|
self.risk_card.set_alarm(False)
|
|
self.risk_pct_label.configure(text_color=colors["orange"])
|
|
self.risk_alarm_active = False
|
|
else:
|
|
self.risk_card.set_alarm(False)
|
|
self.risk_pct_label.configure(text_color=colors["green"])
|
|
self.risk_alarm_active = False
|
|
|
|
except Exception as e:
|
|
logger.debug(f"Risk state update error: {e}")
|
|
|
|
def on_closing(self):
|
|
"""Handle window close"""
|
|
self.running = False
|
|
if self.mt5:
|
|
self.mt5.disconnect()
|
|
self.destroy()
|
|
|
|
|
|
def main():
|
|
app = TradingDashboardPro()
|
|
app.protocol("WM_DELETE_WINDOW", app.on_closing)
|
|
app.mainloop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|