Files
Place-Order-Trading-Bot/position_monitor.py
T
cbazzaandClaude Sonnet 4.6 c959a26dc0 fix: remaining medium-priority issues from code review + log analysis
trading_database.py:
- migrate_from_json: validate exit_time > entry_time before applying exit update
  Trades with exit before entry are logged as open (no invalid exit applied)
  This prevents the timestamp inversion bug that corrupted the DB with 625 bad trades

position_monitor.py:
- Replace fragile datetime.strptime('%Y-%m-%d %H:%M:%S') with fromisoformat()
  Handles both space-separated and ISO 8601 T-separated formats, strips microseconds

trading_bot_gui.py:
- Call infra.log_bot_status('running') on bot start -> bot_status table now populated
- Call infra.log_bot_status('stopped') on bot stop
  Previously bot_status table remained empty (0 rows), making monitoring impossible

telegram_bot_commands_old.py:
- Remove superseded file (replaced by telegram_bot_commands.py)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 12:20:32 +02:00

223 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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")
"""