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
+39 -41
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,24 +127,28 @@ 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:
if abs(exit_price - sl_price) <= tolerance:
return "stop_loss"
return "manual_close"
-539
View File
@@ -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()
+70 -61
View File
@@ -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)
# 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']
}
# 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:
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"""
+56 -21
View File
@@ -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
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: