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>
This commit is contained in:
2026-05-12 12:20:32 +02:00
co-authored by Claude Sonnet 4.6
parent f37e7adcf3
commit c959a26dc0
4 changed files with 172 additions and 669 deletions
+42 -44
View File
@@ -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"