diff --git a/position_monitor.py b/position_monitor.py index bb28096..8df6f57 100644 --- a/position_monitor.py +++ b/position_monitor.py @@ -4,8 +4,8 @@ Überwacht offene Positionen und updated die Datenbank wenn sie geschlossen werden """ -import MetaTrader5 as mt -from datetime import datetime +import MetaTrader5 as mt5 +from datetime import datetime, timedelta, timezone from trading_database import TradingDatabase from telegram_notifier import TelegramNotifier import logging @@ -20,7 +20,6 @@ class PositionMonitor: def __init__(self, database: TradingDatabase, telegram: TelegramNotifier = None): self.db = database self.telegram = telegram - self.tracked_positions = {} # {ticket: position_data} def check_open_positions(self): """ @@ -28,21 +27,19 @@ class PositionMonitor: und vergleicht mit MT5 um geschlossene zu finden """ try: - # Hole alle offenen Positionen aus der Datenbank - open_trades = self.db.get_open_trades() + if not mt5.terminal_info(): + logger.error("MT5 not initialized – skipping position check") + return + open_trades = self.db.get_open_trades() if not open_trades: return - # Hole aktuelle Positionen von MT5 - mt5_positions = mt.positions_get() + mt5_positions = mt5.positions_get() mt5_tickets = {pos.ticket for pos in mt5_positions} if mt5_positions else set() - # Prüfe jede offene Position aus der DB for trade in open_trades: ticket = trade['ticket'] - - # Wenn Position nicht mehr in MT5 → wurde geschlossen if ticket not in mt5_tickets: self._handle_closed_position(ticket, trade) @@ -54,38 +51,35 @@ class PositionMonitor: Behandelt eine geschlossene Position """ try: - # Hole Trade-History von MT5 (letzten 30 Tage) - from datetime import timedelta - now = datetime.now() + now = datetime.now(tz=timezone.utc) days_ago = now - timedelta(days=30) - deals = mt.history_deals_get(days_ago, now, ticket=ticket) + deals = mt5.history_deals_get(days_ago, now, ticket=ticket) if not deals: logger.warning(f"No history found for ticket {ticket}") return - # Finde das Close-Deal (letztes Deal für dieses Ticket) - close_deal = None - for deal in deals: - if deal.entry == 1: # Entry out = Close - close_deal = deal - break - - if not close_deal: + # Collect all close deals (entry==1); partial closes produce multiple + close_deals = [d for d in deals if d.entry == 1] + if not close_deals: logger.warning(f"No close deal found for ticket {ticket}") return - # Berechne Exit-Daten - exit_price = close_deal.price - exit_time = datetime.fromtimestamp(close_deal.time) - profit = close_deal.profit - commission = close_deal.commission - swap = close_deal.swap + # Use the most recent close deal as the authoritative exit + close_deal = max(close_deals, key=lambda d: d.time) - # Berechne weitere Metriken - entry_time = datetime.strptime(trade_data['entry_time'], '%Y-%m-%d %H:%M:%S') + # Convert MT5 UTC timestamp to naive UTC datetime for DB consistency + exit_time = datetime.fromtimestamp(close_deal.time, tz=timezone.utc).replace(tzinfo=None) + + # Sum profit/commission/swap across all close deals (handles partial closes) + profit = sum(d.profit for d in close_deals) + commission = sum(d.commission for d in close_deals) + swap = sum(d.swap for d in close_deals) + + entry_time = datetime.fromisoformat(trade_data['entry_time'].replace('T', ' ').split('.')[0]) duration = (exit_time - entry_time).total_seconds() / 3600 # hours + # commission and swap are already negative in MT5 history net_profit = profit + commission + swap # RR Ratio berechnen (falls SL/TP bekannt) @@ -100,12 +94,12 @@ class PositionMonitor: if risk > 0: rr_ratio = reward / risk - # Bestimme Exit-Grund exit_reason = self._determine_exit_reason( exit_price, trade_data.get('sl_price'), trade_data.get('tp_price'), - trade_data['type'] + trade_data['type'], + trade_data.get('symbol', 'XAUUSD') ) # Update Database @@ -133,25 +127,29 @@ class PositionMonitor: except Exception as e: logger.error(f"Error handling closed position {ticket}: {e}") - def _determine_exit_reason(self, exit_price, sl_price, tp_price, trade_type): + def _get_price_tolerance(self, symbol: str) -> float: + """Returns 1-pip tolerance for the given symbol via MT5, with fallback""" + try: + info = mt5.symbol_info(symbol) + if info: + return info.point * 10 # 1 pip = 10 points on 5-digit brokers + except Exception: + pass + return 0.5 # fallback for XAUUSD + + def _determine_exit_reason(self, exit_price, sl_price, tp_price, trade_type, symbol: str = 'XAUUSD'): """ Bestimmt warum der Trade geschlossen wurde """ if not sl_price or not tp_price: return "manual_close" - tolerance = 0.5 # Pips tolerance + tolerance = self._get_price_tolerance(symbol) - if trade_type == "BUY": - if abs(exit_price - tp_price) <= tolerance: - return "take_profit" - elif abs(exit_price - sl_price) <= tolerance: - return "stop_loss" - else: # SELL - if abs(exit_price - tp_price) <= tolerance: - return "take_profit" - elif abs(exit_price - sl_price) <= tolerance: - return "stop_loss" + if abs(exit_price - tp_price) <= tolerance: + return "take_profit" + if abs(exit_price - sl_price) <= tolerance: + return "stop_loss" return "manual_close" diff --git a/telegram_bot_commands_old.py b/telegram_bot_commands_old.py deleted file mode 100644 index 78e044e..0000000 --- a/telegram_bot_commands_old.py +++ /dev/null @@ -1,539 +0,0 @@ -#!/usr/bin/env python3 -""" -🤖 TELEGRAM BOT COMMANDS - Interactive Bot Control -Erlaubt Remote-Control des Trading Bots via Telegram Commands - -COMMANDS: -/status - Bot Status, offene Positionen, Balance -/pause - Trading pausieren (keine neuen Trades) -/resume - Trading fortsetzen -/close - ALLE offenen Positionen schließen (Emergency) -/stats - Performance Statistiken (heute, Woche, gesamt) -/balance - Aktueller Kontostand + Equity -/help - Liste aller Commands - -USAGE: -1. In separate Cell im Notebook starten: - bot_commander = TelegramBotCommander() - bot_commander.start() - -2. Oder als Background-Service: - python telegram_bot_commands.py -""" - -import threading -import time -import json -import MetaTrader5 as mt5 -from datetime import datetime, timedelta -from typing import Optional, Dict -from telegram import Update, Bot -from telegram.ext import Updater, CommandHandler, CallbackContext -import sqlite3 - -# Import existing modules -from telegram_notifier import TelegramNotifier, load_telegram_config - - -class TradingBotController: - """ - Controller für Trading Bot - ermöglicht Pause/Resume/Close - """ - - def __init__(self): - self.is_paused = False - self.pause_reason = "" - self.pause_timestamp = None - - - def pause_trading(self, reason: str = "Manual pause via Telegram"): - """Pausiert Trading (keine neuen Trades)""" - self.is_paused = True - self.pause_reason = reason - self.pause_timestamp = datetime.now() - - return { - 'success': True, - 'message': f"✅ Trading PAUSED\nReason: {reason}" - } - - - def resume_trading(self): - """Aktiviert Trading wieder""" - if not self.is_paused: - return { - 'success': False, - 'message': "⚠️ Trading is not paused" - } - - pause_duration = datetime.now() - self.pause_timestamp - self.is_paused = False - self.pause_reason = "" - - return { - 'success': True, - 'message': f"✅ Trading RESUMED\nWas paused for: {pause_duration}" - } - - - def close_all_positions(self) -> Dict: - """ - Schließt ALLE offenen Positionen (Emergency Exit) - - Returns: - Dict mit Ergebnis - """ - if not mt5.initialize(): - return { - 'success': False, - 'message': "❌ MT5 connection failed" - } - - positions = mt5.positions_get() - - if not positions: - return { - 'success': True, - 'message': "ℹ️ No open positions to close" - } - - closed_count = 0 - failed_count = 0 - total_profit = 0 - - for position in positions: - # Prepare close request - close_request = { - "action": mt5.TRADE_ACTION_DEAL, - "symbol": position.symbol, - "volume": position.volume, - "type": mt5.ORDER_TYPE_SELL if position.type == mt5.ORDER_TYPE_BUY else mt5.ORDER_TYPE_BUY, - "position": position.ticket, - "price": mt5.symbol_info_tick(position.symbol).bid if position.type == mt5.ORDER_TYPE_BUY else mt5.symbol_info_tick(position.symbol).ask, - "deviation": 20, - "magic": 234000, - "comment": "Emergency close via Telegram", - "type_time": mt5.ORDER_TIME_GTC, - "type_filling": mt5.ORDER_FILLING_IOC, - } - - # Execute close - result = mt5.order_send(close_request) - - if result.retcode == mt5.TRADE_RETCODE_DONE: - closed_count += 1 - total_profit += position.profit - else: - failed_count += 1 - - message = f"🚨 *EMERGENCY CLOSE EXECUTED*\n\n" - message += f"✅ Closed: {closed_count} positions\n" - if failed_count > 0: - message += f"❌ Failed: {failed_count} positions\n" - message += f"💰 Total P&L: ${total_profit:.2f}" - - return { - 'success': True, - 'message': message, - 'closed': closed_count, - 'failed': failed_count, - 'profit': total_profit - } - - - def get_status(self) -> Dict: - """ - Holt aktuellen Bot Status - - Returns: - Dict mit Status-Informationen - """ - if not mt5.initialize(): - return { - 'success': False, - 'message': "❌ MT5 connection failed" - } - - # Account Info - account_info = mt5.account_info() - - # Open Positions - positions = mt5.positions_get() - - # Calculate floating P&L - floating_pl = sum(p.profit for p in positions) if positions else 0 - - # Build status message - status_msg = "📊 *BOT STATUS*\n\n" - - # Trading Status - if self.is_paused: - status_msg += "⏸️ *Status:* PAUSED\n" - status_msg += f"*Reason:* {self.pause_reason}\n" - duration = datetime.now() - self.pause_timestamp - status_msg += f"*Duration:* {duration}\n\n" - else: - status_msg += "✅ *Status:* ACTIVE\n\n" - - # Account Info - status_msg += "💰 *ACCOUNT*\n" - status_msg += f"Balance: ${account_info.balance:.2f}\n" - status_msg += f"Equity: ${account_info.equity:.2f}\n" - status_msg += f"Margin: ${account_info.margin:.2f}\n" - status_msg += f"Free Margin: ${account_info.margin_free:.2f}\n\n" - - # Open Positions - status_msg += f"📈 *POSITIONS*\n" - status_msg += f"Open: {len(positions) if positions else 0}\n" - status_msg += f"Floating P&L: ${floating_pl:.2f}\n\n" - - # Position Details - if positions: - status_msg += "*Open Trades:*\n" - for i, p in enumerate(positions[:5], 1): # Max 5 positions - type_emoji = "🟢" if p.type == mt5.ORDER_TYPE_BUY else "🔴" - status_msg += f"{i}. {type_emoji} {p.symbol} | {p.volume} lots | ${p.profit:.2f}\n" - - if len(positions) > 5: - status_msg += f"... and {len(positions) - 5} more\n" - - status_msg += f"\n⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} UTC" - - return { - 'success': True, - 'message': status_msg, - 'is_paused': self.is_paused, - 'positions': len(positions) if positions else 0, - 'balance': account_info.balance, - 'equity': account_info.equity - } - - - def get_balance(self) -> Dict: - """Holt Balance + Equity Info""" - if not mt5.initialize(): - return { - 'success': False, - 'message': "❌ MT5 connection failed" - } - - account_info = mt5.account_info() - positions = mt5.positions_get() - floating_pl = sum(p.profit for p in positions) if positions else 0 - - message = "💰 *ACCOUNT BALANCE*\n\n" - message += f"Balance: ${account_info.balance:.2f}\n" - message += f"Equity: ${account_info.equity:.2f}\n" - message += f"Floating P&L: ${floating_pl:.2f}\n\n" - message += f"Margin Used: ${account_info.margin:.2f}\n" - message += f"Free Margin: ${account_info.margin_free:.2f}\n" - message += f"Margin Level: {account_info.margin_level:.2f}%\n\n" - message += f"⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} UTC" - - return { - 'success': True, - 'message': message - } - - -class TelegramBotCommander: - """ - Telegram Bot Command Handler - Läuft im Hintergrund und reagiert auf Commands - """ - - def __init__(self, config_file: str = "telegram_config.json"): - """ - Initialize Telegram Bot Commander - - Args: - config_file: Path to telegram config file - """ - # Load config - self.config = load_telegram_config() - - if not self.config or 'bot_token' not in self.config: - raise ValueError("❌ Telegram config not found or invalid") - - self.bot_token = self.config['bot_token'] - self.chat_id = self.config['chat_id'] - - # Initialize Bot - self.bot = Bot(token=self.bot_token) - self.updater = Updater(token=self.bot_token, use_context=True) - - # Controller - self.controller = TradingBotController() - - # Notifier (for sending messages) - self.notifier = TelegramNotifier(self.bot_token, self.chat_id) - - # Database path - self.db_path = "trading_bot.db" - - # Setup command handlers - self._setup_handlers() - - print("✅ Telegram Bot Commander initialized") - print(f"📱 Bot Token: {self.bot_token[:20]}...") - print(f"👤 Chat ID: {self.chat_id}") - - - def _setup_handlers(self): - """Setup command handlers""" - dispatcher = self.updater.dispatcher - - # Register commands - dispatcher.add_handler(CommandHandler("start", self.cmd_start)) - dispatcher.add_handler(CommandHandler("help", self.cmd_help)) - dispatcher.add_handler(CommandHandler("status", self.cmd_status)) - dispatcher.add_handler(CommandHandler("pause", self.cmd_pause)) - dispatcher.add_handler(CommandHandler("resume", self.cmd_resume)) - dispatcher.add_handler(CommandHandler("close", self.cmd_close)) - dispatcher.add_handler(CommandHandler("balance", self.cmd_balance)) - dispatcher.add_handler(CommandHandler("stats", self.cmd_stats)) - - print("✅ Command handlers registered") - - - def start(self): - """Start the bot (blocking)""" - print("🚀 Starting Telegram Bot...") - print("📱 Send /help to see available commands") - - # Send startup message - self.notifier.send_message( - "🤖 *Telegram Bot Commander STARTED*\n\n" - "✅ Bot is now listening for commands\n" - "Send /help for available commands" - ) - - # Start polling - self.updater.start_polling() - print("✅ Bot is running. Press Ctrl+C to stop.") - - # Run until interrupted - self.updater.idle() - - - def start_background(self): - """Start bot in background thread""" - thread = threading.Thread(target=self.start, daemon=True) - thread.start() - print("✅ Telegram Bot running in background") - return thread - - - # ========================================== - # COMMAND HANDLERS - # ========================================== - - def cmd_start(self, update: Update, context: CallbackContext): - """Handle /start command""" - message = ( - "🤖 *Trading Bot Commander*\n\n" - "Welcome! I can help you control your trading bot remotely.\n\n" - "Send /help to see available commands." - ) - update.message.reply_text(message, parse_mode='Markdown') - - - def cmd_help(self, update: Update, context: CallbackContext): - """Handle /help command""" - message = """ -📚 *Available Commands* - -*Bot Control:* -/status - Bot status, positions, balance -/pause - Pause trading (no new trades) -/resume - Resume trading -/close - Close ALL positions (emergency) - -*Information:* -/balance - Account balance & equity -/stats - Performance statistics -/help - Show this help - -*Safety Features:* -⚠️ /close requires confirmation -✅ /pause is instant -🔒 Only authorized user can use commands -""" - update.message.reply_text(message, parse_mode='Markdown') - - - def cmd_status(self, update: Update, context: CallbackContext): - """Handle /status command""" - result = self.controller.get_status() - update.message.reply_text(result['message'], parse_mode='Markdown') - - - def cmd_pause(self, update: Update, context: CallbackContext): - """Handle /pause command""" - result = self.controller.pause_trading("Manual pause via Telegram") - update.message.reply_text(result['message'], parse_mode='Markdown') - - - def cmd_resume(self, update: Update, context: CallbackContext): - """Handle /resume command""" - result = self.controller.resume_trading() - update.message.reply_text(result['message'], parse_mode='Markdown') - - - def cmd_close(self, update: Update, context: CallbackContext): - """Handle /close command""" - # Safety check - require confirmation - args = context.args - - if not args or args[0].lower() != 'confirm': - message = ( - "⚠️ *EMERGENCY CLOSE*\n\n" - "This will close ALL open positions!\n\n" - "To confirm, send:\n" - "`/close confirm`" - ) - update.message.reply_text(message, parse_mode='Markdown') - return - - # Execute close - update.message.reply_text("🚨 Closing all positions...", parse_mode='Markdown') - result = self.controller.close_all_positions() - update.message.reply_text(result['message'], parse_mode='Markdown') - - - def cmd_balance(self, update: Update, context: CallbackContext): - """Handle /balance command""" - result = self.controller.get_balance() - update.message.reply_text(result['message'], parse_mode='Markdown') - - - def cmd_stats(self, update: Update, context: CallbackContext): - """Handle /stats command""" - stats = self._get_performance_stats() - update.message.reply_text(stats, parse_mode='Markdown') - - - # ========================================== - # HELPER FUNCTIONS - # ========================================== - - def _get_performance_stats(self) -> str: - """Get performance statistics from database""" - try: - conn = sqlite3.connect(self.db_path) - cursor = conn.cursor() - - # Today's stats - today_stats = cursor.execute(""" - SELECT - COUNT(*) as trades, - SUM(CASE WHEN net_profit > 0 THEN 1 ELSE 0 END) as wins, - SUM(CASE WHEN net_profit < 0 THEN 1 ELSE 0 END) as losses, - ROUND(SUM(net_profit), 2) as profit - FROM trades - WHERE DATE(entry_time) = DATE('now') - """).fetchone() - - # This week's stats - week_stats = cursor.execute(""" - SELECT - COUNT(*) as trades, - SUM(CASE WHEN net_profit > 0 THEN 1 ELSE 0 END) as wins, - SUM(CASE WHEN net_profit < 0 THEN 1 ELSE 0 END) as losses, - ROUND(SUM(net_profit), 2) as profit - FROM trades - WHERE entry_time >= datetime('now', '-7 days') - """).fetchone() - - # Overall stats - overall_stats = cursor.execute(""" - SELECT - COUNT(*) as trades, - SUM(CASE WHEN net_profit > 0 THEN 1 ELSE 0 END) as wins, - SUM(CASE WHEN net_profit < 0 THEN 1 ELSE 0 END) as losses, - ROUND(SUM(net_profit), 2) as profit, - ROUND(AVG(net_profit), 2) as avg_profit - FROM trades - WHERE net_profit IS NOT NULL - """).fetchone() - - conn.close() - - # Build message - message = "📊 *PERFORMANCE STATISTICS*\n\n" - - # Today - message += "📅 *TODAY*\n" - if today_stats[0] > 0: - today_wr = (today_stats[1] / today_stats[0] * 100) if today_stats[0] > 0 else 0 - message += f"Trades: {today_stats[0]} | WR: {today_wr:.1f}%\n" - message += f"Profit: ${today_stats[3]:.2f}\n\n" - else: - message += "No trades today\n\n" - - # This Week - message += "📈 *THIS WEEK (7 days)*\n" - if week_stats[0] > 0: - week_wr = (week_stats[1] / week_stats[0] * 100) if week_stats[0] > 0 else 0 - message += f"Trades: {week_stats[0]} | WR: {week_wr:.1f}%\n" - message += f"Wins: {week_stats[1]} | Losses: {week_stats[2]}\n" - message += f"Profit: ${week_stats[3]:.2f}\n\n" - else: - message += "No trades this week\n\n" - - # Overall - message += "🎯 *OVERALL*\n" - if overall_stats[0] > 0: - overall_wr = (overall_stats[1] / overall_stats[0] * 100) if overall_stats[0] > 0 else 0 - message += f"Total Trades: {overall_stats[0]}\n" - message += f"Win Rate: {overall_wr:.1f}%\n" - message += f"Total Profit: ${overall_stats[3]:.2f}\n" - message += f"Avg/Trade: ${overall_stats[4]:.2f}\n" - else: - message += "No trade history\n" - - message += f"\n⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} UTC" - - return message - - except Exception as e: - return f"❌ Error fetching stats: {e}" - - -# ========================================== -# GLOBAL CONTROLLER INSTANCE -# ========================================== - -# Global instance that can be accessed from notebook -_global_controller = None - -def get_bot_controller() -> TradingBotController: - """Get global bot controller instance""" - global _global_controller - if _global_controller is None: - _global_controller = TradingBotController() - return _global_controller - - -# ========================================== -# USAGE -# ========================================== - -if __name__ == "__main__": - print("="*70) - print("🤖 TELEGRAM BOT COMMANDER - Starting") - print("="*70) - - try: - # Create bot commander - commander = TelegramBotCommander() - - # Start bot (blocking) - commander.start() - - except KeyboardInterrupt: - print("\n⏸️ Bot stopped by user") - except Exception as e: - print(f"❌ Error: {e}") - import traceback - traceback.print_exc() diff --git a/trading_bot_gui.py b/trading_bot_gui.py index 3fffd92..6201618 100644 --- a/trading_bot_gui.py +++ b/trading_bot_gui.py @@ -6,13 +6,17 @@ Professional Desktop App für Trading Bot V1.9 import tkinter as tk from tkinter import ttk, scrolledtext, messagebox -import MetaTrader5 as mt +import MetaTrader5 as mt5 import threading import queue from datetime import datetime import logging from apscheduler.schedulers.background import BackgroundScheduler +# MT5 connection config — edit here instead of in code +MT5_LOGIN = 10800246 +MT5_SERVER = 'VantageInternational-Demo' + # Trading Bot Imports from infrastructure_patch import TradingInfrastructure, create_scheduled_reports from position_monitor import PositionMonitor @@ -66,6 +70,9 @@ class TradingBotGUI: # Start GUI update loop self.process_queue() + # Clean shutdown when window is closed + self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + def create_widgets(self): """Create all GUI widgets""" @@ -358,54 +365,50 @@ class TradingBotGUI: logger.info(message) def process_queue(self): - """Process GUI updates from queue""" + """Process GUI updates from queue (runs in main thread — thread-safe)""" try: while True: - item = self.gui_queue.get_nowait() - action, data = item - + action, data = self.gui_queue.get_nowait() if action == "log": self.log_text.insert(tk.END, data) self.log_text.see(tk.END) + elif action == "widget": + # data = (widget, config_dict) e.g. (self.connection_status, {"text": "...", "fg": "green"}) + widget, kwargs = data + widget.config(**kwargs) elif action == "status": self.update_status_display(data) - except queue.Empty: pass - - # Schedule next check self.root.after(100, self.process_queue) + def _update_widget(self, widget, **kwargs): + """Queue a thread-safe widget update""" + self.gui_queue.put(("widget", (widget, kwargs))) + def connect_mt5(self): """Connect to MT5""" def connect_thread(): try: self.log("Connecting to MT5...") - if not mt.initialize(): + if not mt5.initialize(): self.log("MT5 initialization failed!", "ERROR") return - # Login (you'll need to add your credentials) import keyring as kr - login = 10800246 - server = 'VantageInternational-Demo' - password = kr.get_password(server, str(login)) + password = kr.get_password(MT5_SERVER, str(MT5_LOGIN)) - if not mt.login(login, password, server): - self.log(f"MT5 login failed: {mt.last_error()}", "ERROR") + if not mt5.login(MT5_LOGIN, password, MT5_SERVER): + self.log(f"MT5 login failed: {mt5.last_error()}", "ERROR") return - account_info = mt.account_info() + account_info = mt5.account_info() if account_info: - self.gui_queue.put(("log", f"[{datetime.now().strftime('%H:%M:%S')}] INFO: ✅ Connected to MT5\n")) - self.gui_queue.put(("log", f"[{datetime.now().strftime('%H:%M:%S')}] INFO: Account: {account_info.login}\n")) - self.gui_queue.put(("log", f"[{datetime.now().strftime('%H:%M:%S')}] INFO: Balance: ${account_info.balance:.2f}\n")) - - # Update GUI - self.connection_status.config(text="✅ Connected", fg="green") - self.account_info.config(text=f"Account: {account_info.login} | Balance: ${account_info.balance:.2f}") - self.connect_btn.config(state=tk.DISABLED) + self.log(f"✅ Connected — Account: {account_info.login} | Balance: ${account_info.balance:.2f}") + self._update_widget(self.connection_status, text="✅ Connected", fg="green") + self._update_widget(self.account_info, text=f"Account: {account_info.login} | Balance: ${account_info.balance:.2f}") + self._update_widget(self.connect_btn, state=tk.DISABLED) except Exception as e: self.log(f"Connection error: {e}", "ERROR") @@ -414,6 +417,10 @@ class TradingBotGUI: def start_bot(self): """Start Trading Bot""" + if self.bot_running: + self.log("Bot is already running", "WARNING") + return + def start_thread(): try: self.log("🚀 Starting Trading Bot...") @@ -476,19 +483,21 @@ class TradingBotGUI: self.bot_running = True self.log("✅ Trading Bot Started Successfully!") + self._update_widget(self.bot_status_label, text="✅ Bot Running", fg="green") + self._update_widget(self.start_btn, state=tk.DISABLED) + self._update_widget(self.stop_btn, state=tk.NORMAL) - # Update GUI - self.bot_status_label.config(text="✅ Bot Running", fg="green") - self.start_btn.config(state=tk.DISABLED) - self.stop_btn.config(state=tk.NORMAL) + bot_config = { + 'version': 'V1.9', + 'enabled_sessions': SESSION_WHITELIST_CONFIG['enabled_sessions'], + 'base_confidence': SESSION_WHITELIST_CONFIG['base_confidence'] + } + + # Log bot status to DB so bot_status table is populated + self.infra.log_bot_status('running', bot_config) # Send Telegram notification if self.infra.telegram: - bot_config = { - 'version': 'V1.9', - 'enabled_sessions': SESSION_WHITELIST_CONFIG['enabled_sessions'], - 'base_confidence': SESSION_WHITELIST_CONFIG['base_confidence'] - } self.infra.send_bot_started(bot_config) except Exception as e: @@ -502,6 +511,8 @@ class TradingBotGUI: self.scheduler.shutdown() self.bot_running = False self.log("🛑 Trading Bot Stopped") + if self.infra: + self.infra.log_bot_status('stopped') # Update GUI self.bot_status_label.config(text="⏸️ Bot Stopped", fg="orange") @@ -531,18 +542,15 @@ class TradingBotGUI: self.log("📊 Checking Status...") - # Session session = self.rhythm_manager.get_current_session() - self.current_session.config(text=session.upper()) + self._update_widget(self.current_session, text=session.upper()) - # Interval interval = self.rhythm_manager.calculate_optimal_interval() - self.current_interval.config(text=f"{interval} min") + self._update_widget(self.current_interval, text=f"{interval} min") - # Positions - positions = mt.positions_get(symbol=self.symbol) + positions = mt5.positions_get(symbol=self.symbol) count = len(positions) if positions else 0 - self.positions_count.config(text=f"{count}/{self.max_positions}") + self._update_widget(self.positions_count, text=f"{count}/{self.max_positions}") # Drawdown Status if self.drawdown_protection: @@ -550,7 +558,7 @@ class TradingBotGUI: status_text = f"Trading: {'✅ Allowed' if status['trading_allowed'] else '🛑 Paused'}\n" status_text += f"Daily: ${status['daily_loss']:.2f}/${status['daily_limit']:.2f}\n" status_text += f"Consecutive: {status['consecutive_losses']}/{status['consecutive_limit']}" - self.drawdown_status.config(text=status_text) + self._update_widget(self.drawdown_status, text=status_text) self.log("✅ Status updated") @@ -563,63 +571,57 @@ class TradingBotGUI: """Check open positions""" def positions_thread(): try: - positions = mt.positions_get(symbol=self.symbol) - + positions = mt5.positions_get(symbol=self.symbol) if not positions: self.log("No open positions") return - self.log(f"📊 {len(positions)} Open Position(s):") for pos in positions: self.log(f" Ticket: {pos.ticket} | {pos.type} | P/L: ${pos.profit:.2f}") - except Exception as e: self.log(f"Error checking positions: {e}", "ERROR") - threading.Thread(target=positions_thread, daemon=True).start() def close_all_positions(self): """Close all open positions""" - confirm = messagebox.askyesno( - "Confirm Close", - "Are you sure you want to close ALL positions?" - ) - + confirm = messagebox.askyesno("Confirm Close", "Are you sure you want to close ALL positions?") if not confirm: return def close_thread(): try: - positions = mt.positions_get(symbol=self.symbol) - + positions = mt5.positions_get(symbol=self.symbol) if not positions: self.log("No positions to close") return self.log(f"Closing {len(positions)} position(s)...") + # Fetch tick once outside loop + tick = mt5.symbol_info_tick(self.symbol) + if not tick: + self.log("Could not get current tick price", "ERROR") + return for pos in positions: + close_price = tick.bid if pos.type == 0 else tick.ask close_request = { - "action": mt.TRADE_ACTION_DEAL, + "action": mt5.TRADE_ACTION_DEAL, "symbol": self.symbol, "volume": pos.volume, - "type": mt.ORDER_TYPE_SELL if pos.type == 0 else mt.ORDER_TYPE_BUY, + "type": mt5.ORDER_TYPE_SELL if pos.type == 0 else mt5.ORDER_TYPE_BUY, "position": pos.ticket, - "price": mt.symbol_info_tick(self.symbol).bid if pos.type == 0 else mt.symbol_info_tick(self.symbol).ask, + "price": close_price, "deviation": 20, "magic": 234000, "comment": "Manual close from GUI", - "type_time": mt.ORDER_TIME_GTC, - "type_filling": mt.ORDER_FILLING_IOC, + "type_time": mt5.ORDER_TIME_GTC, + "type_filling": mt5.ORDER_FILLING_IOC, } - - result = mt.order_send(close_request) - - if result.retcode == mt.TRADE_RETCODE_DONE: + result = mt5.order_send(close_request) + if result.retcode == mt5.TRADE_RETCODE_DONE: self.log(f"✅ Closed position {pos.ticket}") else: self.log(f"❌ Failed to close {pos.ticket}: {result.comment}", "ERROR") - except Exception as e: self.log(f"Error closing positions: {e}", "ERROR") @@ -629,6 +631,13 @@ class TradingBotGUI: """Update status display from data""" pass + def on_closing(self): + """Shutdown scheduler and MT5 before closing""" + if self.scheduler and self.scheduler.running: + self.scheduler.shutdown(wait=False) + mt5.shutdown() + self.root.destroy() + def main(): """Main Entry Point""" diff --git a/trading_database.py b/trading_database.py index ce38467..1dbf053 100644 --- a/trading_database.py +++ b/trading_database.py @@ -12,10 +12,13 @@ FEATURES: import sqlite3 import json +import logging from datetime import datetime, timedelta from typing import Dict, List, Optional, Tuple import os +logger = logging.getLogger(__name__) + class TradingDatabase: """ @@ -32,8 +35,12 @@ class TradingDatabase: self.db_path = db_path self.conn = None self.cursor = None - self._connect() - self._create_tables() + try: + self._connect() + self._create_tables() + except Exception: + self.close() + raise def _connect(self): """Establish database connection""" @@ -179,6 +186,15 @@ class TradingDatabase: ON trades(confidence) """) + # Key-value store for persistent bot settings (e.g. drawdown pause state) + self.cursor.execute(""" + CREATE TABLE IF NOT EXISTS bot_settings ( + key TEXT PRIMARY KEY, + value TEXT, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """) + self.conn.commit() @@ -292,8 +308,6 @@ class TradingDatabase: commission: Commission paid swap: Swap paid """ - from datetime import datetime - # Calculate duration if we have entry_time duration_hours = None try: @@ -307,8 +321,8 @@ class TradingDatabase: else: exit_dt = exit_time duration_hours = (exit_dt - entry_time).total_seconds() / 3600 - except: - pass + except Exception as e: + logger.warning(f"Could not calculate duration for ticket {ticket}: {e}") net_profit = profit - commission - swap @@ -557,14 +571,7 @@ class TradingDatabase: Returns: Dictionary with comprehensive statistics """ - date_filter = "" - params = [] - - if days: - date_filter = "AND entry_time >= datetime('now', '-' || ? || ' days')" - params.append(days) - - query = f""" + query = """ SELECT COUNT(*) as total_trades, SUM(CASE WHEN net_profit > 0 THEN 1 ELSE 0 END) as wins, @@ -580,8 +587,11 @@ class TradingDatabase: ROUND(AVG(duration_hours), 2) as avg_duration FROM trades WHERE status = 'closed' - {date_filter} """ + params = [] + if days: + query += " AND entry_time >= datetime('now', '-' || ? || ' days')" + params.append(days) self.cursor.execute(query, params) row = self.cursor.fetchone() @@ -639,24 +649,49 @@ class TradingDatabase: skipped += 1 continue - # Insert trade + # Validate timestamp order before inserting + entry_str = trade.get('entry_time') + exit_str = trade.get('exit_time') + if entry_str and exit_str: + try: + entry_dt = datetime.fromisoformat(str(entry_str)) + exit_dt = datetime.fromisoformat(str(exit_str)) + if exit_dt < entry_dt: + logger.warning( + f"Skipping exit update for ticket {trade.get('ticket')}: " + f"exit_time ({exit_str}) is before entry_time ({entry_str})" + ) + exit_str = None # insert as open, don't apply invalid exit + except (ValueError, TypeError): + pass + self.log_trade_entry(trade) - # If trade is closed, update exit data - if trade.get('exit_time'): - self.update_trade_exit( - trade['ticket'], - trade - ) + if exit_str: + self.update_trade_exit(trade['ticket'], trade) migrated += 1 except Exception as e: - print(f"⚠️ Error migrating trade {trade.get('ticket')}: {e}") + logger.warning(f"Error migrating trade {trade.get('ticket')}: {e}") print(f"✅ Migration complete: {migrated} trades migrated, {skipped} skipped") + def save_setting(self, key: str, value: str): + """Persist a key-value setting across restarts""" + self.cursor.execute(""" + INSERT OR REPLACE INTO bot_settings (key, value, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + """, (key, value)) + self.conn.commit() + + def load_setting(self, key: str, default: Optional[str] = None) -> Optional[str]: + """Load a persisted setting, returns default if not found""" + self.cursor.execute("SELECT value FROM bot_settings WHERE key = ?", (key,)) + row = self.cursor.fetchone() + return row['value'] if row else default + def close(self): """Close database connection""" if self.conn: