#!/usr/bin/env python3 """ 📊 Position Monitor - Trade Exit Detection Überwacht offene Positionen und updated die Datenbank wenn sie geschlossen werden """ import MetaTrader5 as mt5 from datetime import datetime, timedelta, timezone from trading_database import TradingDatabase from telegram_notifier import TelegramNotifier import logging logger = logging.getLogger(__name__) class PositionMonitor: """ Überwacht offene Positionen und erkennt wenn sie geschlossen werden """ def __init__(self, database: TradingDatabase, telegram: TelegramNotifier = None): self.db = database self.telegram = telegram def check_open_positions(self): """ Prüft alle offenen Positionen in der Datenbank und vergleicht mit MT5 um geschlossene zu finden """ try: 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 mt5_positions = mt5.positions_get() mt5_tickets = {pos.ticket for pos in mt5_positions} if mt5_positions else set() for trade in open_trades: ticket = trade['ticket'] if ticket not in mt5_tickets: self._handle_closed_position(ticket, trade) except Exception as e: logger.error(f"Error checking positions: {e}") def _handle_closed_position(self, ticket, trade_data): """ Behandelt eine geschlossene Position """ try: now = datetime.now(tz=timezone.utc) days_ago = now - timedelta(days=30) deals = mt5.history_deals_get(days_ago, now, ticket=ticket) if not deals: logger.warning(f"No history found for ticket {ticket}") return # 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 # Use the most recent close deal as the authoritative exit close_deal = max(close_deals, key=lambda d: d.time) # 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) rr_ratio = None if trade_data.get('sl_price') and trade_data.get('tp_price'): entry_price = trade_data['entry_price'] sl_price = trade_data['sl_price'] tp_price = trade_data['tp_price'] risk = abs(entry_price - sl_price) reward = abs(tp_price - entry_price) if risk > 0: rr_ratio = reward / risk exit_reason = self._determine_exit_reason( exit_price, trade_data.get('sl_price'), trade_data.get('tp_price'), trade_data['type'], trade_data.get('symbol', 'XAUUSD') ) # Update Database exit_data = { 'exit_price': exit_price, 'exit_time': exit_time.strftime('%Y-%m-%d %H:%M:%S'), 'duration_hours': duration, 'profit': profit, 'commission': commission, 'swap': swap, 'net_profit': net_profit, 'rr_ratio': rr_ratio, 'status': 'closed', 'exit_reason': exit_reason } self.db.update_trade_exit(ticket, exit_data) logger.info(f"✅ Updated closed position {ticket}: {exit_reason}, Profit: {net_profit:.2f}") # Sende Telegram Notification if self.telegram: self._send_exit_notification(ticket, trade_data, exit_data) except Exception as e: logger.error(f"Error handling closed position {ticket}: {e}") 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 = self._get_price_tolerance(symbol) if abs(exit_price - tp_price) <= tolerance: return "take_profit" if abs(exit_price - sl_price) <= tolerance: return "stop_loss" return "manual_close" def _send_exit_notification(self, ticket, trade_data, exit_data): """ Sendet Telegram Notification für geschlossenen Trade """ try: notification_data = { 'ticket': ticket, 'symbol': trade_data['symbol'], 'type': trade_data['type'], 'entry_price': trade_data['entry_price'], 'exit_price': exit_data['exit_price'], 'exit_time': exit_data['exit_time'], 'exit_reason': exit_data['exit_reason'], 'net_profit': exit_data['net_profit'], 'duration_hours': exit_data['duration_hours'], 'session': trade_data.get('session', 'unknown') } self.telegram.notify_trade_exit(notification_data) except Exception as e: logger.error(f"Error sending exit notification: {e}") def create_position_monitor_job(database, telegram=None): """ Factory-Funktion die einen Position Monitor Job erstellt Usage: monitor = PositionMonitor(infra.db, infra.telegram) # Im Scheduler hinzufügen: scheduler.add_job( func=monitor.check_open_positions, trigger='interval', minutes=1, # Prüfe jede Minute id='position_monitor' ) """ monitor = PositionMonitor(database, telegram) return monitor # ========================================== # USAGE EXAMPLE (für Notebook) # ========================================== """ Im Trading Bot Notebook hinzufügen: # Nach Infrastructure Setup (Cell 8): from position_monitor import PositionMonitor # Position Monitor erstellen position_monitor = PositionMonitor(infra.db, infra.telegram) # Im Scheduler hinzufügen (Cell 33): scheduler.add_job( func=position_monitor.check_open_positions, trigger='interval', minutes=1, id='position_monitor' ) print("✅ Position Monitor aktiviert - prüft jede Minute nach geschlossenen Trades") """