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:
+42
-44
@@ -4,8 +4,8 @@
|
|||||||
Überwacht offene Positionen und updated die Datenbank wenn sie geschlossen werden
|
Überwacht offene Positionen und updated die Datenbank wenn sie geschlossen werden
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import MetaTrader5 as mt
|
import MetaTrader5 as mt5
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta, timezone
|
||||||
from trading_database import TradingDatabase
|
from trading_database import TradingDatabase
|
||||||
from telegram_notifier import TelegramNotifier
|
from telegram_notifier import TelegramNotifier
|
||||||
import logging
|
import logging
|
||||||
@@ -20,7 +20,6 @@ class PositionMonitor:
|
|||||||
def __init__(self, database: TradingDatabase, telegram: TelegramNotifier = None):
|
def __init__(self, database: TradingDatabase, telegram: TelegramNotifier = None):
|
||||||
self.db = database
|
self.db = database
|
||||||
self.telegram = telegram
|
self.telegram = telegram
|
||||||
self.tracked_positions = {} # {ticket: position_data}
|
|
||||||
|
|
||||||
def check_open_positions(self):
|
def check_open_positions(self):
|
||||||
"""
|
"""
|
||||||
@@ -28,21 +27,19 @@ class PositionMonitor:
|
|||||||
und vergleicht mit MT5 um geschlossene zu finden
|
und vergleicht mit MT5 um geschlossene zu finden
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Hole alle offenen Positionen aus der Datenbank
|
if not mt5.terminal_info():
|
||||||
open_trades = self.db.get_open_trades()
|
logger.error("MT5 not initialized – skipping position check")
|
||||||
|
return
|
||||||
|
|
||||||
|
open_trades = self.db.get_open_trades()
|
||||||
if not open_trades:
|
if not open_trades:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Hole aktuelle Positionen von MT5
|
mt5_positions = mt5.positions_get()
|
||||||
mt5_positions = mt.positions_get()
|
|
||||||
mt5_tickets = {pos.ticket for pos in mt5_positions} if mt5_positions else set()
|
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:
|
for trade in open_trades:
|
||||||
ticket = trade['ticket']
|
ticket = trade['ticket']
|
||||||
|
|
||||||
# Wenn Position nicht mehr in MT5 → wurde geschlossen
|
|
||||||
if ticket not in mt5_tickets:
|
if ticket not in mt5_tickets:
|
||||||
self._handle_closed_position(ticket, trade)
|
self._handle_closed_position(ticket, trade)
|
||||||
|
|
||||||
@@ -54,38 +51,35 @@ class PositionMonitor:
|
|||||||
Behandelt eine geschlossene Position
|
Behandelt eine geschlossene Position
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# Hole Trade-History von MT5 (letzten 30 Tage)
|
now = datetime.now(tz=timezone.utc)
|
||||||
from datetime import timedelta
|
|
||||||
now = datetime.now()
|
|
||||||
days_ago = now - timedelta(days=30)
|
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:
|
if not deals:
|
||||||
logger.warning(f"No history found for ticket {ticket}")
|
logger.warning(f"No history found for ticket {ticket}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Finde das Close-Deal (letztes Deal für dieses Ticket)
|
# Collect all close deals (entry==1); partial closes produce multiple
|
||||||
close_deal = None
|
close_deals = [d for d in deals if d.entry == 1]
|
||||||
for deal in deals:
|
if not close_deals:
|
||||||
if deal.entry == 1: # Entry out = Close
|
|
||||||
close_deal = deal
|
|
||||||
break
|
|
||||||
|
|
||||||
if not close_deal:
|
|
||||||
logger.warning(f"No close deal found for ticket {ticket}")
|
logger.warning(f"No close deal found for ticket {ticket}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Berechne Exit-Daten
|
# Use the most recent close deal as the authoritative exit
|
||||||
exit_price = close_deal.price
|
close_deal = max(close_deals, key=lambda d: d.time)
|
||||||
exit_time = datetime.fromtimestamp(close_deal.time)
|
|
||||||
profit = close_deal.profit
|
|
||||||
commission = close_deal.commission
|
|
||||||
swap = close_deal.swap
|
|
||||||
|
|
||||||
# Berechne weitere Metriken
|
# Convert MT5 UTC timestamp to naive UTC datetime for DB consistency
|
||||||
entry_time = datetime.strptime(trade_data['entry_time'], '%Y-%m-%d %H:%M:%S')
|
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
|
duration = (exit_time - entry_time).total_seconds() / 3600 # hours
|
||||||
|
|
||||||
|
# commission and swap are already negative in MT5 history
|
||||||
net_profit = profit + commission + swap
|
net_profit = profit + commission + swap
|
||||||
|
|
||||||
# RR Ratio berechnen (falls SL/TP bekannt)
|
# RR Ratio berechnen (falls SL/TP bekannt)
|
||||||
@@ -100,12 +94,12 @@ class PositionMonitor:
|
|||||||
if risk > 0:
|
if risk > 0:
|
||||||
rr_ratio = reward / risk
|
rr_ratio = reward / risk
|
||||||
|
|
||||||
# Bestimme Exit-Grund
|
|
||||||
exit_reason = self._determine_exit_reason(
|
exit_reason = self._determine_exit_reason(
|
||||||
exit_price,
|
exit_price,
|
||||||
trade_data.get('sl_price'),
|
trade_data.get('sl_price'),
|
||||||
trade_data.get('tp_price'),
|
trade_data.get('tp_price'),
|
||||||
trade_data['type']
|
trade_data['type'],
|
||||||
|
trade_data.get('symbol', 'XAUUSD')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update Database
|
# Update Database
|
||||||
@@ -133,25 +127,29 @@ class PositionMonitor:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error handling closed position {ticket}: {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
|
Bestimmt warum der Trade geschlossen wurde
|
||||||
"""
|
"""
|
||||||
if not sl_price or not tp_price:
|
if not sl_price or not tp_price:
|
||||||
return "manual_close"
|
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:
|
||||||
if abs(exit_price - tp_price) <= tolerance:
|
return "take_profit"
|
||||||
return "take_profit"
|
if abs(exit_price - sl_price) <= tolerance:
|
||||||
elif abs(exit_price - sl_price) <= tolerance:
|
return "stop_loss"
|
||||||
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"
|
|
||||||
|
|
||||||
return "manual_close"
|
return "manual_close"
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
+72
-63
@@ -6,13 +6,17 @@ Professional Desktop App für Trading Bot V1.9
|
|||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, scrolledtext, messagebox
|
from tkinter import ttk, scrolledtext, messagebox
|
||||||
import MetaTrader5 as mt
|
import MetaTrader5 as mt5
|
||||||
import threading
|
import threading
|
||||||
import queue
|
import queue
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
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
|
# Trading Bot Imports
|
||||||
from infrastructure_patch import TradingInfrastructure, create_scheduled_reports
|
from infrastructure_patch import TradingInfrastructure, create_scheduled_reports
|
||||||
from position_monitor import PositionMonitor
|
from position_monitor import PositionMonitor
|
||||||
@@ -66,6 +70,9 @@ class TradingBotGUI:
|
|||||||
# Start GUI update loop
|
# Start GUI update loop
|
||||||
self.process_queue()
|
self.process_queue()
|
||||||
|
|
||||||
|
# Clean shutdown when window is closed
|
||||||
|
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||||
|
|
||||||
def create_widgets(self):
|
def create_widgets(self):
|
||||||
"""Create all GUI widgets"""
|
"""Create all GUI widgets"""
|
||||||
|
|
||||||
@@ -358,54 +365,50 @@ class TradingBotGUI:
|
|||||||
logger.info(message)
|
logger.info(message)
|
||||||
|
|
||||||
def process_queue(self):
|
def process_queue(self):
|
||||||
"""Process GUI updates from queue"""
|
"""Process GUI updates from queue (runs in main thread — thread-safe)"""
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
item = self.gui_queue.get_nowait()
|
action, data = self.gui_queue.get_nowait()
|
||||||
action, data = item
|
|
||||||
|
|
||||||
if action == "log":
|
if action == "log":
|
||||||
self.log_text.insert(tk.END, data)
|
self.log_text.insert(tk.END, data)
|
||||||
self.log_text.see(tk.END)
|
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":
|
elif action == "status":
|
||||||
self.update_status_display(data)
|
self.update_status_display(data)
|
||||||
|
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Schedule next check
|
|
||||||
self.root.after(100, self.process_queue)
|
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):
|
def connect_mt5(self):
|
||||||
"""Connect to MT5"""
|
"""Connect to MT5"""
|
||||||
def connect_thread():
|
def connect_thread():
|
||||||
try:
|
try:
|
||||||
self.log("Connecting to MT5...")
|
self.log("Connecting to MT5...")
|
||||||
|
|
||||||
if not mt.initialize():
|
if not mt5.initialize():
|
||||||
self.log("MT5 initialization failed!", "ERROR")
|
self.log("MT5 initialization failed!", "ERROR")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Login (you'll need to add your credentials)
|
|
||||||
import keyring as kr
|
import keyring as kr
|
||||||
login = 10800246
|
password = kr.get_password(MT5_SERVER, str(MT5_LOGIN))
|
||||||
server = 'VantageInternational-Demo'
|
|
||||||
password = kr.get_password(server, str(login))
|
|
||||||
|
|
||||||
if not mt.login(login, password, server):
|
if not mt5.login(MT5_LOGIN, password, MT5_SERVER):
|
||||||
self.log(f"MT5 login failed: {mt.last_error()}", "ERROR")
|
self.log(f"MT5 login failed: {mt5.last_error()}", "ERROR")
|
||||||
return
|
return
|
||||||
|
|
||||||
account_info = mt.account_info()
|
account_info = mt5.account_info()
|
||||||
if account_info:
|
if account_info:
|
||||||
self.gui_queue.put(("log", f"[{datetime.now().strftime('%H:%M:%S')}] INFO: ✅ Connected to MT5\n"))
|
self.log(f"✅ Connected — Account: {account_info.login} | Balance: ${account_info.balance:.2f}")
|
||||||
self.gui_queue.put(("log", f"[{datetime.now().strftime('%H:%M:%S')}] INFO: Account: {account_info.login}\n"))
|
self._update_widget(self.connection_status, text="✅ Connected", fg="green")
|
||||||
self.gui_queue.put(("log", f"[{datetime.now().strftime('%H:%M:%S')}] INFO: Balance: ${account_info.balance:.2f}\n"))
|
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)
|
||||||
# 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)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Connection error: {e}", "ERROR")
|
self.log(f"Connection error: {e}", "ERROR")
|
||||||
@@ -414,6 +417,10 @@ class TradingBotGUI:
|
|||||||
|
|
||||||
def start_bot(self):
|
def start_bot(self):
|
||||||
"""Start Trading Bot"""
|
"""Start Trading Bot"""
|
||||||
|
if self.bot_running:
|
||||||
|
self.log("Bot is already running", "WARNING")
|
||||||
|
return
|
||||||
|
|
||||||
def start_thread():
|
def start_thread():
|
||||||
try:
|
try:
|
||||||
self.log("🚀 Starting Trading Bot...")
|
self.log("🚀 Starting Trading Bot...")
|
||||||
@@ -476,19 +483,21 @@ class TradingBotGUI:
|
|||||||
|
|
||||||
self.bot_running = True
|
self.bot_running = True
|
||||||
self.log("✅ Trading Bot Started Successfully!")
|
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
|
bot_config = {
|
||||||
self.bot_status_label.config(text="✅ Bot Running", fg="green")
|
'version': 'V1.9',
|
||||||
self.start_btn.config(state=tk.DISABLED)
|
'enabled_sessions': SESSION_WHITELIST_CONFIG['enabled_sessions'],
|
||||||
self.stop_btn.config(state=tk.NORMAL)
|
'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
|
# Send Telegram notification
|
||||||
if self.infra.telegram:
|
if self.infra.telegram:
|
||||||
bot_config = {
|
|
||||||
'version': 'V1.9',
|
|
||||||
'enabled_sessions': SESSION_WHITELIST_CONFIG['enabled_sessions'],
|
|
||||||
'base_confidence': SESSION_WHITELIST_CONFIG['base_confidence']
|
|
||||||
}
|
|
||||||
self.infra.send_bot_started(bot_config)
|
self.infra.send_bot_started(bot_config)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -502,6 +511,8 @@ class TradingBotGUI:
|
|||||||
self.scheduler.shutdown()
|
self.scheduler.shutdown()
|
||||||
self.bot_running = False
|
self.bot_running = False
|
||||||
self.log("🛑 Trading Bot Stopped")
|
self.log("🛑 Trading Bot Stopped")
|
||||||
|
if self.infra:
|
||||||
|
self.infra.log_bot_status('stopped')
|
||||||
|
|
||||||
# Update GUI
|
# Update GUI
|
||||||
self.bot_status_label.config(text="⏸️ Bot Stopped", fg="orange")
|
self.bot_status_label.config(text="⏸️ Bot Stopped", fg="orange")
|
||||||
@@ -531,18 +542,15 @@ class TradingBotGUI:
|
|||||||
|
|
||||||
self.log("📊 Checking Status...")
|
self.log("📊 Checking Status...")
|
||||||
|
|
||||||
# Session
|
|
||||||
session = self.rhythm_manager.get_current_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()
|
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 = mt5.positions_get(symbol=self.symbol)
|
||||||
positions = mt.positions_get(symbol=self.symbol)
|
|
||||||
count = len(positions) if positions else 0
|
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
|
# Drawdown Status
|
||||||
if self.drawdown_protection:
|
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"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"Daily: ${status['daily_loss']:.2f}/${status['daily_limit']:.2f}\n"
|
||||||
status_text += f"Consecutive: {status['consecutive_losses']}/{status['consecutive_limit']}"
|
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")
|
self.log("✅ Status updated")
|
||||||
|
|
||||||
@@ -563,63 +571,57 @@ class TradingBotGUI:
|
|||||||
"""Check open positions"""
|
"""Check open positions"""
|
||||||
def positions_thread():
|
def positions_thread():
|
||||||
try:
|
try:
|
||||||
positions = mt.positions_get(symbol=self.symbol)
|
positions = mt5.positions_get(symbol=self.symbol)
|
||||||
|
|
||||||
if not positions:
|
if not positions:
|
||||||
self.log("No open positions")
|
self.log("No open positions")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log(f"📊 {len(positions)} Open Position(s):")
|
self.log(f"📊 {len(positions)} Open Position(s):")
|
||||||
for pos in positions:
|
for pos in positions:
|
||||||
self.log(f" Ticket: {pos.ticket} | {pos.type} | P/L: ${pos.profit:.2f}")
|
self.log(f" Ticket: {pos.ticket} | {pos.type} | P/L: ${pos.profit:.2f}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Error checking positions: {e}", "ERROR")
|
self.log(f"Error checking positions: {e}", "ERROR")
|
||||||
|
|
||||||
threading.Thread(target=positions_thread, daemon=True).start()
|
threading.Thread(target=positions_thread, daemon=True).start()
|
||||||
|
|
||||||
def close_all_positions(self):
|
def close_all_positions(self):
|
||||||
"""Close all open positions"""
|
"""Close all open positions"""
|
||||||
confirm = messagebox.askyesno(
|
confirm = messagebox.askyesno("Confirm Close", "Are you sure you want to close ALL positions?")
|
||||||
"Confirm Close",
|
|
||||||
"Are you sure you want to close ALL positions?"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not confirm:
|
if not confirm:
|
||||||
return
|
return
|
||||||
|
|
||||||
def close_thread():
|
def close_thread():
|
||||||
try:
|
try:
|
||||||
positions = mt.positions_get(symbol=self.symbol)
|
positions = mt5.positions_get(symbol=self.symbol)
|
||||||
|
|
||||||
if not positions:
|
if not positions:
|
||||||
self.log("No positions to close")
|
self.log("No positions to close")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log(f"Closing {len(positions)} position(s)...")
|
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:
|
for pos in positions:
|
||||||
|
close_price = tick.bid if pos.type == 0 else tick.ask
|
||||||
close_request = {
|
close_request = {
|
||||||
"action": mt.TRADE_ACTION_DEAL,
|
"action": mt5.TRADE_ACTION_DEAL,
|
||||||
"symbol": self.symbol,
|
"symbol": self.symbol,
|
||||||
"volume": pos.volume,
|
"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,
|
"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,
|
"deviation": 20,
|
||||||
"magic": 234000,
|
"magic": 234000,
|
||||||
"comment": "Manual close from GUI",
|
"comment": "Manual close from GUI",
|
||||||
"type_time": mt.ORDER_TIME_GTC,
|
"type_time": mt5.ORDER_TIME_GTC,
|
||||||
"type_filling": mt.ORDER_FILLING_IOC,
|
"type_filling": mt5.ORDER_FILLING_IOC,
|
||||||
}
|
}
|
||||||
|
result = mt5.order_send(close_request)
|
||||||
result = mt.order_send(close_request)
|
if result.retcode == mt5.TRADE_RETCODE_DONE:
|
||||||
|
|
||||||
if result.retcode == mt.TRADE_RETCODE_DONE:
|
|
||||||
self.log(f"✅ Closed position {pos.ticket}")
|
self.log(f"✅ Closed position {pos.ticket}")
|
||||||
else:
|
else:
|
||||||
self.log(f"❌ Failed to close {pos.ticket}: {result.comment}", "ERROR")
|
self.log(f"❌ Failed to close {pos.ticket}: {result.comment}", "ERROR")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log(f"Error closing positions: {e}", "ERROR")
|
self.log(f"Error closing positions: {e}", "ERROR")
|
||||||
|
|
||||||
@@ -629,6 +631,13 @@ class TradingBotGUI:
|
|||||||
"""Update status display from data"""
|
"""Update status display from data"""
|
||||||
pass
|
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():
|
def main():
|
||||||
"""Main Entry Point"""
|
"""Main Entry Point"""
|
||||||
|
|||||||
+58
-23
@@ -12,10 +12,13 @@ FEATURES:
|
|||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Dict, List, Optional, Tuple
|
from typing import Dict, List, Optional, Tuple
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TradingDatabase:
|
class TradingDatabase:
|
||||||
"""
|
"""
|
||||||
@@ -32,8 +35,12 @@ class TradingDatabase:
|
|||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
self.conn = None
|
self.conn = None
|
||||||
self.cursor = None
|
self.cursor = None
|
||||||
self._connect()
|
try:
|
||||||
self._create_tables()
|
self._connect()
|
||||||
|
self._create_tables()
|
||||||
|
except Exception:
|
||||||
|
self.close()
|
||||||
|
raise
|
||||||
|
|
||||||
def _connect(self):
|
def _connect(self):
|
||||||
"""Establish database connection"""
|
"""Establish database connection"""
|
||||||
@@ -179,6 +186,15 @@ class TradingDatabase:
|
|||||||
ON trades(confidence)
|
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()
|
self.conn.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -292,8 +308,6 @@ class TradingDatabase:
|
|||||||
commission: Commission paid
|
commission: Commission paid
|
||||||
swap: Swap paid
|
swap: Swap paid
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# Calculate duration if we have entry_time
|
# Calculate duration if we have entry_time
|
||||||
duration_hours = None
|
duration_hours = None
|
||||||
try:
|
try:
|
||||||
@@ -307,8 +321,8 @@ class TradingDatabase:
|
|||||||
else:
|
else:
|
||||||
exit_dt = exit_time
|
exit_dt = exit_time
|
||||||
duration_hours = (exit_dt - entry_time).total_seconds() / 3600
|
duration_hours = (exit_dt - entry_time).total_seconds() / 3600
|
||||||
except:
|
except Exception as e:
|
||||||
pass
|
logger.warning(f"Could not calculate duration for ticket {ticket}: {e}")
|
||||||
|
|
||||||
net_profit = profit - commission - swap
|
net_profit = profit - commission - swap
|
||||||
|
|
||||||
@@ -557,14 +571,7 @@ class TradingDatabase:
|
|||||||
Returns:
|
Returns:
|
||||||
Dictionary with comprehensive statistics
|
Dictionary with comprehensive statistics
|
||||||
"""
|
"""
|
||||||
date_filter = ""
|
query = """
|
||||||
params = []
|
|
||||||
|
|
||||||
if days:
|
|
||||||
date_filter = "AND entry_time >= datetime('now', '-' || ? || ' days')"
|
|
||||||
params.append(days)
|
|
||||||
|
|
||||||
query = f"""
|
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as total_trades,
|
COUNT(*) as total_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 wins,
|
||||||
@@ -580,8 +587,11 @@ class TradingDatabase:
|
|||||||
ROUND(AVG(duration_hours), 2) as avg_duration
|
ROUND(AVG(duration_hours), 2) as avg_duration
|
||||||
FROM trades
|
FROM trades
|
||||||
WHERE status = 'closed'
|
WHERE status = 'closed'
|
||||||
{date_filter}
|
|
||||||
"""
|
"""
|
||||||
|
params = []
|
||||||
|
if days:
|
||||||
|
query += " AND entry_time >= datetime('now', '-' || ? || ' days')"
|
||||||
|
params.append(days)
|
||||||
|
|
||||||
self.cursor.execute(query, params)
|
self.cursor.execute(query, params)
|
||||||
row = self.cursor.fetchone()
|
row = self.cursor.fetchone()
|
||||||
@@ -639,24 +649,49 @@ class TradingDatabase:
|
|||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
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)
|
self.log_trade_entry(trade)
|
||||||
|
|
||||||
# If trade is closed, update exit data
|
if exit_str:
|
||||||
if trade.get('exit_time'):
|
self.update_trade_exit(trade['ticket'], trade)
|
||||||
self.update_trade_exit(
|
|
||||||
trade['ticket'],
|
|
||||||
trade
|
|
||||||
)
|
|
||||||
|
|
||||||
migrated += 1
|
migrated += 1
|
||||||
|
|
||||||
except Exception as e:
|
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")
|
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):
|
def close(self):
|
||||||
"""Close database connection"""
|
"""Close database connection"""
|
||||||
if self.conn:
|
if self.conn:
|
||||||
|
|||||||
Reference in New Issue
Block a user