2025-12-16 22:02:15 +01:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""
|
|
|
|
|
|
📊 Position Monitor - Trade Exit Detection
|
|
|
|
|
|
Überwacht offene Positionen und updated die Datenbank wenn sie geschlossen werden
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-05-12 12:20:32 +02:00
|
|
|
|
import MetaTrader5 as mt5
|
|
|
|
|
|
from datetime import datetime, timedelta, timezone
|
2025-12-16 22:02:15 +01:00
|
|
|
|
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:
|
2026-05-12 12:20:32 +02:00
|
|
|
|
if not mt5.terminal_info():
|
|
|
|
|
|
logger.error("MT5 not initialized – skipping position check")
|
|
|
|
|
|
return
|
2025-12-16 22:02:15 +01:00
|
|
|
|
|
2026-05-12 12:20:32 +02:00
|
|
|
|
open_trades = self.db.get_open_trades()
|
2025-12-16 22:02:15 +01:00
|
|
|
|
if not open_trades:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-05-12 12:20:32 +02:00
|
|
|
|
mt5_positions = mt5.positions_get()
|
2025-12-16 22:02:15 +01:00
|
|
|
|
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:
|
2026-05-12 12:20:32 +02:00
|
|
|
|
now = datetime.now(tz=timezone.utc)
|
2025-12-16 22:02:15 +01:00
|
|
|
|
days_ago = now - timedelta(days=30)
|
2026-05-12 12:20:32 +02:00
|
|
|
|
deals = mt5.history_deals_get(days_ago, now, ticket=ticket)
|
2025-12-16 22:02:15 +01:00
|
|
|
|
|
|
|
|
|
|
if not deals:
|
|
|
|
|
|
logger.warning(f"No history found for ticket {ticket}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-05-12 12:20:32 +02:00
|
|
|
|
# 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:
|
2025-12-16 22:02:15 +01:00
|
|
|
|
logger.warning(f"No close deal found for ticket {ticket}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-05-12 12:20:32 +02:00
|
|
|
|
# Use the most recent close deal as the authoritative exit
|
|
|
|
|
|
close_deal = max(close_deals, key=lambda d: d.time)
|
2025-12-16 22:02:15 +01:00
|
|
|
|
|
2026-05-12 12:20:32 +02:00
|
|
|
|
# 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])
|
2025-12-16 22:02:15 +01:00
|
|
|
|
duration = (exit_time - entry_time).total_seconds() / 3600 # hours
|
|
|
|
|
|
|
2026-05-12 12:20:32 +02:00
|
|
|
|
# commission and swap are already negative in MT5 history
|
2025-12-16 22:02:15 +01:00
|
|
|
|
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'),
|
2026-05-12 12:20:32 +02:00
|
|
|
|
trade_data['type'],
|
|
|
|
|
|
trade_data.get('symbol', 'XAUUSD')
|
2025-12-16 22:02:15 +01:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 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}")
|
|
|
|
|
|
|
2026-05-12 12:20:32 +02:00
|
|
|
|
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'):
|
2025-12-16 22:02:15 +01:00
|
|
|
|
"""
|
|
|
|
|
|
Bestimmt warum der Trade geschlossen wurde
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not sl_price or not tp_price:
|
|
|
|
|
|
return "manual_close"
|
|
|
|
|
|
|
2026-05-12 12:20:32 +02:00
|
|
|
|
tolerance = self._get_price_tolerance(symbol)
|
2025-12-16 22:02:15 +01:00
|
|
|
|
|
2026-05-12 12:20:32 +02:00
|
|
|
|
if abs(exit_price - tp_price) <= tolerance:
|
|
|
|
|
|
return "take_profit"
|
|
|
|
|
|
if abs(exit_price - sl_price) <= tolerance:
|
|
|
|
|
|
return "stop_loss"
|
2025-12-16 22:02:15 +01:00
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
"""
|