import tkinter as tk
from tkinter import messagebox, scrolledtext, ttk
import MetaTrader5 as mt5
import pandas as pd
import threading
import time
from datetime import datetime
import os
import configparser
# --- CONFIGURACIÓN DE LA APLICACIÓN (Variables globales por defecto) ---
DEFAULT_MT5_LOGIN = 12345678 # ¡CAMBIA ESTO CON TU LOGIN REAL DE MT5!
DEFAULT_MT5_PASSWORD = "Tu_Password_MT5" # ¡CAMBIA ESTO CON TU CONTRASEÑA REAL DE
MT5!
DEFAULT_MT5_SERVER = "Exness-MT5Trial11" # Servidor por defecto, preferiblemente
uno de demo
DEFAULT_SYMBOL = "EURUSDm" # Símbolo por defecto para el display de precio (el bot
escaneará múltiples)
DEFAULT_INTERVAL = '1min' # Temporalidad por defecto para el análisis de señales
# Configuración para la Estrategia Donchian
DONCHIAN_PERIOD = 20 # Período para la estrategia de Donchian
DEFAULT_EXPIRATION_MINUTES = 5 # Vencimiento recomendado para la operación
# --- CONFIGURACIONES PARA ANÁLISIS TÉCNICO MEJORADO ---
USE_RSI_CONFIRMATION = True
RSI_PERIOD = 14
RSI_OVERBOUGHT = 70
RSI_OVERSOLD = 30
USE_SMA_TREND_CONFIRMATION = True
SMA_FAST_PERIOD = 20 # SMA rápida para cruce y confirmación de tendencia a
corto/medio
SMA_SLOW_PERIOD = 50 # SMA lenta para confirmación de tendencia a medio/largo
# --- NUEVAS CONFIGURACIONES PARA VOLUMEN ---
USE_VOLUME_CONFIRMATION = True # Habilitar o deshabilitar confirmación por volumen
VOLUME_SMA_PERIOD = 50 # Período para la Media Móvil del Volumen
VOLUME_MULTIPLIER_THRESHOLD = 1.5 # Cuántas veces el volumen actual debe ser mayor
que la VMA
# --- NUEVAS CONFIGURACIONES PARA ADX ---
USE_ADX_CONFIRMATION = True # Habilitar o deshabilitar confirmación por ADX
ADX_PERIOD = 14 # Período para el cálculo del ADX
ADX_THRESHOLD = 25 # Umbral por encima del cual se considera una tendencia fuerte
# --- CONFIGURACIONES PARA SL/TP (AHORA CON OPCIÓN DINÁMICA) ---
USE_DYNAMIC_SL_TP = False # Por defecto, usar pips fijos
DEFAULT_SL_PIPS = 50 # Pips para el Stop Loss por defecto (si no es dinámico)
DEFAULT_TP_PIPS = 100 # Pips para el Take Profit por defecto (si no es dinámico)
DYNAMIC_SL_TP_BUFFER_PIPS = 10 # Pips de buffer para el SL dinámico (ej. 5-10 pips)
DYNAMIC_RISK_REWARD_RATIO = 2.0 # Ratio Riesgo/Beneficio por defecto (ej. 1.5, 2.0)
# --- NUEVA CONFIGURACIÓN: Límite de Operaciones Simultáneas ---
MAX_SIMULTANEOUS_TRADES = 6
# --- Nombre del archivo de configuración de credenciales ---
CREDENTIALS_FILE = "[Link]" # Archivo para guardar credenciales
# --- ACTUALIZADO: Lista de servidores comunes de Exness ---
EXNESS_SERVERS = [
"Exness-MT5Trial11", # Nuevo servidor Demo
"Exness-Trial", # Servidor Demo existente
"Exness-MT5Real11", # Nuevo servidor Real
"Exness-Real", # Servidor Real existente
"Exness-Real2",
"Exness-Real3",
"Exness-Real4",
]
# --- MODIFICADO: Lista de símbolos específicos para monitorear con los sufijos 'm'
---
SPECIFIC_SYMBOLS_TO_MONITOR = [
"EURUSDm",
"XAUUSDm",
"BTCUSDm",
"USTECm",
"AAPLm",
"GBPUSDm",
"USDJPYm",
"AUDUSDm",
"USDCADm",
"USDCHFm",
]
# --- FUNCIONES DE MT5 (Para ejecutar en un hilo separado) ---
class MT5Client:
def __init__(self, log_callback=None, auto_mode_var=None,
get_strategy_params_callback=None):
[Link] = False
self.log_callback = log_callback
self.shutdown_event = [Link]() # Para el bucle principal de datos
de mercado
self.balance_shutdown_event = [Link]() # Para el bucle de
actualización de balance
self.signal_event = [Link]()
self.signal_confirmed = False
self.auto_mode_var = auto_mode_var
self.monitored_symbols = [] # Aquí se almacenarán los símbolos que se
escanearán
self.get_strategy_params_callback = get_strategy_params_callback
self.open_trades = {} # Diccionario para rastrear operaciones abiertas por
el bot {position_ticket: {'symbol':..., 'direction':..., 'gui_item_id':...}}
self.pending_signals = [] # Cola para señales pendientes cuando el límite
de operaciones se alcanza
def _log(self, message):
if self.log_callback and callable(self.log_callback):
[Link](0, self.log_callback, message)
else:
print(f"[{[Link]().strftime('%H:%M:%S')}] {message}")
def connect_mt5(self, login, password, server):
self._log(f"Intentando inicializar MT5 con login: {login}, servidor:
{server}...")
# Shutdown any previous MT5 connection before initializing a new one
if [Link]:
[Link]()
[Link] = False # Reset connection state
if not [Link](login=login, password=password, server=server):
self._log(f"Error al inicializar MT5: {mt5.last_error()}")
[Link] = False
return False
else:
self._log("MetaTrader5 inicializado y conectado correctamente.")
[Link] = True
return True
def disconnect_mt5(self):
if [Link]:
[Link]()
self._log("MetaTrader5 desconectado.")
[Link] = False
self.open_trades = {} # Limpiar operaciones abiertas al desconectar
self.pending_signals = [] # Limpiar señales pendientes
self.monitored_symbols = [] # Limpiar la lista de símbolos monitoreados
def get_account_info(self):
if not [Link]:
return None
account_info = mt5.account_info()
if account_info:
return {
"balance": account_info.balance,
"equity": account_info.equity,
"profit": account_info.profit,
"margin": account_info.margin,
"currency": account_info.currency,
"server": account_info.server
}
else:
if not self.shutdown_event.is_set() and not
self.balance_shutdown_event.is_set():
# Solo loguear el error si no estamos en proceso de cierre
self._log(f"Error al obtener información de la cuenta:
{mt5.last_error()}")
return None
def get_latest_price(self, symbol):
if not [Link]:
return None
symbol_info = mt5.symbol_info_tick(symbol)
if symbol_info:
return symbol_info.last
else:
return None
def fetch_ohlcv(self, symbol, timeframe, count):
if not [Link]:
return None
mt5_timeframe = None
if timeframe == '1min': mt5_timeframe = mt5.TIMEFRAME_M1
elif timeframe == '5min': mt5_timeframe = mt5.TIMEFRAME_M5
elif timeframe == '15min': mt5_timeframe = mt5.TIMEFRAME_M15
elif timeframe == '30min': mt5_timeframe = mt5.TIMEFRAME_M30
elif timeframe == '1hour': mt5_timeframe = mt5.TIMEFRAME_H1
else:
self._log(f"Temporalidad '{timeframe}' no soportada por MT5.")
return None
symbol_info = mt5.symbol_info(symbol)
if symbol_info is None:
self._log(f"Error: Símbolo {symbol} no encontrado en MT5.")
return None
if not symbol_info.visible:
if not mt5.symbol_select(symbol, True):
self._log(f"Error: No se pudo seleccionar el símbolo {symbol} para
obtener datos históricos.")
return None
params = self.get_strategy_params_callback()
# Calcular el número mínimo de velas necesarias para TODOS los indicadores
required_candles_for_indicators = 0
if params['use_rsi']:
required_candles_for_indicators = max(required_candles_for_indicators,
params['rsi_period'] + 5)
if params['use_sma']: # Ahora usamos dos SMAs, así que necesitamos el
período mayor + buffer
required_candles_for_indicators = max(required_candles_for_indicators,
params['sma_slow_period'] + 5)
# Nuevo: para volumen
if params['use_volume_confirmation']:
required_candles_for_indicators = max(required_candles_for_indicators,
params['volume_sma_period'] + 5)
# Nuevo: para ADX. ADX requiere más datos, típicamente 2 * ADX_PERIOD + un
buffer
if params['use_adx']:
required_candles_for_indicators = max(required_candles_for_indicators,
params['adx_period'] * 2 + 5)
required_candles = max(count, params['donchian_period'] + 2,
required_candles_for_indicators)
rates = mt5.copy_rates_from_pos(symbol, mt5_timeframe, 0, required_candles)
if rates is None or not [Link]:
if not self.shutdown_event.is_set():
self._log(f"No se pudieron obtener datos para {symbol}
({timeframe}): {mt5.last_error()}")
return None
rates_frame = [Link](rates)
rates_frame['time'] = pd.to_datetime(rates_frame['time'], unit='s')
rates_frame = rates_frame.set_index('time')
return rates_frame[['open', 'high', 'low', 'close', 'tick_volume']]
def calculate_rsi(self, df: [Link], period: int) -> [Link]:
# Asegurarse de que hay suficientes datos para el cálculo
if len(df) < period + 1:
return [Link]([None] * len(df), index=[Link])
delta = df['close'].diff()
gain = [Link](delta > 0, 0)
loss = -[Link](delta < 0, 0)
avg_gain = [Link](com=period - 1, adjust=False).mean()
avg_loss = [Link](com=period - 1, adjust=False).mean()
# Evitar división por cero
rs = avg_gain / avg_loss.replace(0, 1e-10) # Pequeño valor para evitar div
by zero
rsi = 100 - (100 / (1 + rs))
return rsi
def calculate_sma(self, df: [Link], column: str, period: int) ->
[Link]:
# Reutilizamos esta función para SMA de precio y ahora para SMA de volumen
if len(df) < period:
return [Link]([None] * len(df), index=[Link])
return df[column].rolling(window=period).mean()
def calculate_adx(self, df: [Link], period: int) -> tuple[[Link],
[Link], [Link]]:
# True Range (TR)
high_low = df['high'] - df['low']
high_close_prev = abs(df['high'] - df['close'].shift(1))
low_close_prev = abs(df['low'] - df['close'].shift(1))
tr = [Link]([high_low, high_close_prev, low_close_prev],
axis=1).max(axis=1)
atr = [Link](com=period - 1, adjust=False).mean()
# Directional Movement (DM) - Recalculado para mayor precisión
plus_dm = df['high'].diff()
minus_dm = df['low'].diff() * -1
plus_dm[plus_dm < 0] = 0
minus_dm[minus_dm < 0] = 0
# Ajuste para que solo el mayor movimiento direccional del período sea
considerado
# Si +DM es mayor que -DM, entonces -DM se hace 0, y viceversa
dm_df = [Link]({'plus_dm': plus_dm, 'minus_dm': minus_dm})
plus_dm = dm_df.apply(lambda x: x['plus_dm'] if x['plus_dm'] >
x['minus_dm'] else 0, axis=1)
minus_dm = dm_df.apply(lambda x: x['minus_dm'] if x['minus_dm'] >
x['plus_dm'] else 0, axis=1)
# Exponentially Weighted Moving Average (EWMA) of DM
plus_di = (plus_dm.ewm(com=period - 1, adjust=False).mean() / atr) * 100
minus_di = (minus_dm.ewm(com=period - 1, adjust=False).mean() / atr) * 100
# Handle division by zero for DI
di_sum = (plus_di + minus_di).replace(0, 1e-10)
dx = abs(plus_di - minus_di) / di_sum * 100
adx = [Link](com=period - 1, adjust=False).mean()
# Manejar posibles NaNs iniciales
if len(df) < period * 2: # ADX necesita un período para DI y luego otro
para ADX
return [Link]([None] * len(df), index=[Link]), \
[Link]([None] * len(df), index=[Link]), \
[Link]([None] * len(df), index=[Link])
return adx, plus_di, minus_di
def generate_donchian_signal(self, ohlcv_df: [Link], current_symbol: str,
current_interval: str) -> dict:
params = self.get_strategy_params_callback()
# Calcular el mínimo de velas necesario para todos los indicadores y el
Donchian
min_candles_required = max(
params['donchian_period'] + 2, # Donchian necesita al menos 2 velas
anteriores
params['rsi_period'] + 10 if params['use_rsi'] else 0, # RSI necesita
el período + algunas velas para estabilizar
params['sma_slow_period'] + 5 if params['use_sma'] else 0, # SMA más
lenta + buffer
params['volume_sma_period'] + 5 if params['use_volume_confirmation']
else 0, # VMA necesita su período + buffer
params['adx_period'] * 2 + 5 if params['use_adx'] else 0 # ADX necesita
suficientes velas
)
if ohlcv_df is None or ohlcv_df.empty or len(ohlcv_df) <
min_candles_required:
if ohlcv_df is not None and len(ohlcv_df) < min_candles_required:
self._log(f"Insuficientes velas para {current_symbol}
({current_interval}). Requeridas: {min_candles_required}, Obtenidas:
{len(ohlcv_df)}. Ajustando fetch_ohlcv puede ayudar.")
return None
# Eliminar NaN introducidos por .shift() u otros cálculos iniciales
ohlcv_df = ohlcv_df.dropna()
if ohlcv_df.empty or len(ohlcv_df) < min_candles_required:
self._log(f"Datos de OHLCV insuficientes o contienen NaN después de
limpieza para {current_symbol}.")
return None
# --- Donchian Channel Calculation ---
ohlcv_df['donchian_upper'] =
ohlcv_df['high'].rolling(window=params['donchian_period']).max()
ohlcv_df['donchian_lower'] =
ohlcv_df['low'].rolling(window=params['donchian_period']).min()
# Verificar si hay suficientes datos después de calcular Donchian
if [Link](ohlcv_df['donchian_upper'].iloc[-2]) or
[Link](ohlcv_df['donchian_lower'].iloc[-2]):
self._log(f"Cálculo de Donchian fallido para {current_symbol} con
período {params['donchian_period']}. Datos insuficientes o error.")
return None
# Usamos los valores de la vela ANTERIOR para identificar la ruptura
previous_donchian_upper = ohlcv_df['donchian_upper'].iloc[-2]
previous_donchian_lower = ohlcv_df['donchian_lower'].iloc[-2]
current_candle_high = ohlcv_df['high'].iloc[-1]
current_candle_low = ohlcv_df['low'].iloc[-1]
current_close = ohlcv_df['close'].iloc[-1]
current_volume = ohlcv_df['tick_volume'].iloc[-1]
signal = None
rsi_value = None
sma_fast_value = None
sma_slow_value = None
vma_value = None
adx_value = None
plus_di_value = None
minus_di_value = None
# --- RSI Confirmation ---
rsi_ok = True
if params['use_rsi']:
if len(ohlcv_df) >= params['rsi_period'] + 1:
ohlcv_df['rsi'] = self.calculate_rsi(ohlcv_df,
params['rsi_period'])
rsi_value = ohlcv_df['rsi'].iloc[-1]
if [Link](rsi_value):
rsi_ok = False
self._log(f"Advertencia: RSI no pudo ser calculado para
{current_symbol} (NaN).")
else:
rsi_ok = False
self._log(f"Advertencia: No hay suficientes datos para calcular RSI
para {current_symbol}.")
# --- SMA Trend Confirmation ---
sma_ok = True
if params['use_sma']:
if len(ohlcv_df) >= params['sma_slow_period']:
ohlcv_df['sma_fast'] = self.calculate_sma(ohlcv_df, 'close',
params['sma_fast_period'])
ohlcv_df['sma_slow'] = self.calculate_sma(ohlcv_df, 'close',
params['sma_slow_period'])
sma_fast_value = ohlcv_df['sma_fast'].iloc[-1]
sma_slow_value = ohlcv_df['sma_slow'].iloc[-1]
if [Link](sma_fast_value) or [Link](sma_slow_value):
sma_ok = False
self._log(f"Advertencia: SMA(s) no pudieron ser calculadas para
{current_symbol} (NaN).")
else:
sma_ok = False
self._log(f"Advertencia: No hay suficientes datos para calcular
SMA(s) para {current_symbol}.")
# --- VOLUME Confirmation ---
volume_ok = True
if params['use_volume_confirmation']:
if len(ohlcv_df) >= params['volume_sma_period']:
ohlcv_df['vma'] = self.calculate_sma(ohlcv_df, 'tick_volume',
params['volume_sma_period'])
vma_value = ohlcv_df['vma'].iloc[-1]
if [Link](vma_value):
volume_ok = False
self._log(f"Advertencia: VMA no pudo ser calculada para
{current_symbol} (NaN).")
elif current_volume < vma_value *
params['volume_multiplier_threshold']:
volume_ok = False
self._log(f"Señal para {current_symbol} rechazada por Volumen.
Actual: {current_volume}, VMA: {vma_value:.0f}. Umbral:
x{params['volume_multiplier_threshold']}.")
else:
volume_ok = False
self._log(f"Advertencia: No hay suficientes datos para calcular VMA
para {current_symbol}.")
# --- ADX Confirmation ---
adx_ok = True
if params['use_adx']:
if len(ohlcv_df) >= params['adx_period'] * 2: # ADX necesita
suficientes velas para calcular bien
adx_series, plus_di_series, minus_di_series =
self.calculate_adx(ohlcv_df, params['adx_period'])
adx_value = adx_series.iloc[-1]
plus_di_value = plus_di_series.iloc[-1]
minus_di_value = minus_di_series.iloc[-1]
if [Link](adx_value) or [Link](plus_di_value) or
[Link](minus_di_value):
adx_ok = False
self._log(f"Advertencia: ADX/DI no pudieron ser calculados para
{current_symbol} (NaN).")
# La lógica de confirmación ADX se aplica dentro de los bloques de
BUY/SELL
else:
adx_ok = False
self._log(f"Advertencia: No hay suficientes datos para calcular
ADX/DI para {current_symbol}.")
# --- LÓGICA DE SEÑAL con Múltiples Confirmaciones (Mejorado) ---
is_buy_breakout = current_candle_high > previous_donchian_upper
is_sell_breakout = current_candle_low < previous_donchian_lower
if is_buy_breakout: # Potencial señal de COMPRA
self._log(f"Posible ruptura de COMPRA para {current_symbol}:
High({current_candle_high:.5f}) > Donchian Upper Anterior
({previous_donchian_upper:.5f}).")
# 1. Confirmación de Tendencia (SMA Crossover o Precio sobre SMAs)
trend_confirmed_buy = True
if params['use_sma']:
if not sma_ok:
trend_confirmed_buy = False
self._log(f"Señal BUY para {current_symbol} rechazada: SMA no
pudo ser calculada o datos insuficientes.")
elif not (sma_fast_value > sma_slow_value and current_close >
sma_slow_value):
trend_confirmed_buy = False
self._log(f"Señal BUY para {current_symbol} rechazada por SMA
(No es tendencia alcista confirmada: Fast:{sma_fast_value:.5f}, Slow:
{sma_slow_value:.5f}, Close:{current_close:.5f}).")
# 2. Confirmación RSI (No sobrecomprado)
rsi_confirmed_buy = True
if params['use_rsi'] and rsi_ok:
if rsi_value > params['rsi_overbought']: # Demasiado alto para una
entrada
rsi_confirmed_buy = False
self._log(f"Señal BUY para {current_symbol} rechazada por RSI
({rsi_value:.2f} > {params['rsi_overbought']} - Sobrecomprado).")
elif params['use_rsi'] and not rsi_ok:
rsi_confirmed_buy = False
# 3. Confirmación de Volumen
volume_confirmed_buy = True
if params['use_volume_confirmation'] and not volume_ok:
volume_confirmed_buy = False
# 4. Confirmación de ADX
adx_confirmed_buy = True
if params['use_adx']:
if not adx_ok:
adx_confirmed_buy = False
self._log(f"Señal BUY para {current_symbol} rechazada: ADX no
pudo ser calculado o datos insuficientes.")
elif not (adx_value > params['adx_threshold'] and plus_di_value >
minus_di_value):
adx_confirmed_buy = False
self._log(f"Señal BUY para {current_symbol} rechazada por ADX
(ADX:{adx_value:.2f} <= {params['adx_threshold']} o +DI:{plus_di_value:.2f} <= -DI:
{minus_di_value:.2f}).")
# Condición final para señal de COMPRA
if trend_confirmed_buy and rsi_confirmed_buy and volume_confirmed_buy
and adx_confirmed_buy:
signal = {
"symbol": current_symbol,
"direction": "BUY",
"entry_price": current_close,
"strategy": "Donchian + SMA + RSI + Volumen + ADX", #
Actualizar nombre de estrategia
"expiration_minutes": DEFAULT_EXPIRATION_MINUTES,
"donchian_upper": previous_donchian_upper,
"donchian_lower": previous_donchian_lower,
"current_rsi": f"{rsi_value:.2f}" if rsi_value is not None else
"N/A",
"sma_fast": f"{sma_fast_value:.5f}" if sma_fast_value is not
None else "N/A",
"sma_slow": f"{sma_slow_value:.5f}" if sma_slow_value is not
None else "N/A",
"current_volume": f"{current_volume:.0f}",
"vma_volume": f"{vma_value:.0f}" if vma_value is not None else
"N/A",
"adx_value": f"{adx_value:.2f}" if adx_value is not None else
"N/A",
"plus_di": f"{plus_di_value:.2f}" if plus_di_value is not None
else "N/A",
"minus_di": f"{minus_di_value:.2f}" if minus_di_value is not
None else "N/A"
}
self._log(f"Señal BUY CONFIRMADA para {current_symbol}. Cierre:
{current_close:.5f}. RSI: {signal['current_rsi']}, SMA Fast: {signal['sma_fast']},
SMA Slow: {signal['sma_slow']}, Volumen: {signal['current_volume']} (VMA:
{signal['vma_volume']}), ADX: {signal['adx_value']} (+DI:{signal['plus_di']}, -DI:
{signal['minus_di']}).")
elif is_sell_breakout: # Potencial señal de VENTA
self._log(f"Posible ruptura de VENTA para {current_symbol}:
Low({current_candle_low:.5f}) < Donchian Lower Anterior
({previous_donchian_lower:.5f}).")
# 1. Confirmación de Tendencia (SMA Crossover o Precio debajo de SMAs)
trend_confirmed_sell = True
if params['use_sma']:
if not sma_ok:
trend_confirmed_sell = False
self._log(f"Señal SELL para {current_symbol} rechazada: SMA no
pudo ser calculada o datos insuficientes.")
elif not (sma_fast_value < sma_slow_value and current_close <
sma_slow_value):
trend_confirmed_sell = False
self._log(f"Señal SELL para {current_symbol} rechazada por SMA
(No es tendencia bajista confirmada: Fast:{sma_fast_value:.5f}, Slow:
{sma_slow_value:.5f}, Close:{current_close:.5f}).")
# 2. Confirmación RSI (No sobrevendido)
rsi_confirmed_sell = True
if params['use_rsi'] and rsi_ok:
if rsi_value < params['rsi_oversold']: # Demasiado bajo para una
entrada
rsi_confirmed_sell = False
self._log(f"Señal SELL para {current_symbol} rechazada por RSI
({rsi_value:.2f} < {params['rsi_oversold']} - Sobrevendido).")
elif params['use_rsi'] and not rsi_ok:
rsi_confirmed_sell = False
# 3. Confirmación de Volumen
volume_confirmed_sell = True
if params['use_volume_confirmation'] and not volume_ok:
volume_confirmed_sell = False
# 4. Confirmación de ADX
adx_confirmed_sell = True
if params['use_adx']:
if not adx_ok:
adx_confirmed_sell = False
self._log(f"Señal SELL para {current_symbol} rechazada: ADX no
pudo ser calculado o datos insuficientes.")
elif not (adx_value > params['adx_threshold'] and minus_di_value >
plus_di_value):
adx_confirmed_sell = False
self._log(f"Señal SELL para {current_symbol} rechazada por ADX
(ADX:{adx_value:.2f} <= {params['adx_threshold']} o -DI:{minus_di_value:.2f} <=
+DI:{plus_di_value:.2f}).")
# Condición final para señal de VENTA
if trend_confirmed_sell and rsi_confirmed_sell and
volume_confirmed_sell and adx_confirmed_sell:
signal = {
"symbol": current_symbol,
"direction": "SELL",
"entry_price": current_close,
"strategy": "Donchian + SMA + RSI + Volumen + ADX", #
Actualizar nombre de estrategia
"expiration_minutes": DEFAULT_EXPIRATION_MINUTES,
"donchian_upper": previous_donchian_upper,
"donchian_lower": previous_donchian_lower,
"current_rsi": f"{rsi_value:.2f}" if rsi_value is not None else
"N/A",
"sma_fast": f"{sma_fast_value:.5f}" if sma_fast_value is not
None else "N/A",
"sma_slow": f"{sma_slow_value:.5f}" if sma_slow_value is not
None else "N/A",
"current_volume": f"{current_volume:.0f}",
"vma_volume": f"{vma_value:.0f}" if vma_value is not None else
"N/A",
"adx_value": f"{adx_value:.2f}" if adx_value is not None else
"N/A",
"plus_di": f"{plus_di_value:.2f}" if plus_di_value is not None
else "N/A",
"minus_di": f"{minus_di_value:.2f}" if minus_di_value is not
None else "N/A"
}
self._log(f"Señal SELL CONFIRMADA para {current_symbol}. Cierre:
{current_close:.5f}. RSI: {signal['current_rsi']}, SMA Fast: {signal['sma_fast']},
SMA Slow: {signal['sma_slow']}, Volumen: {signal['current_volume']} (VMA:
{signal['vma_volume']}), ADX: {signal['adx_value']} (+DI:{signal['plus_di']}, -DI:
{signal['minus_di']}).")
return signal
def send_order(self, signal_data, gui_item_id):
symbol = signal_data['symbol']
order_type = signal_data['direction']
if not [Link]:
self._log("Error: No se puede enviar orden, MT5 no está conectado.")
[Link](0, app.update_trade_result, gui_item_id, "FALLO (MT5
Desconectado)", "")
return None
strategy_params = self.get_strategy_params_callback()
trade_volume = strategy_params['trade_volume']
if trade_volume is None or trade_volume <= 0:
self._log("Error: Volumen de operación no válido o cero. No se envió la
orden.")
[Link](0, app.update_trade_result, gui_item_id, "FALLO
(Volumen Inválido)", "")
return None
symbol_info = mt5.symbol_info(symbol)
if symbol_info is None:
self._log(f"Error: Símbolo {symbol} no encontrado o no válido.")
[Link](0, app.update_trade_result, gui_item_id, "FALLO
(Símbolo Inválido)", "")
return None
if not symbol_info.visible:
if not mt5.symbol_select(symbol, True):
self._log(f"Error: No se pudo seleccionar el símbolo {symbol} para
trading.")
[Link](0, app.update_trade_result, gui_item_id, "FALLO
(Símbolo No Seleccionable)", "")
return None
tick = mt5.symbol_info_tick(symbol)
if tick is None:
self._log(f"Error: No se pudo obtener el tick para el símbolo
{symbol}.")
[Link](0, app.update_trade_result, gui_item_id, "FALLO (Sin
Tick de Precio)", "")
return None
point = mt5.symbol_info(symbol).point
sl = 0.0
tp = 0.0
# Usar Bid para ventas y Ask para compras
current_price = [Link] if order_type == "BUY" else [Link]
# --- Lógica de SL/TP Dinámico o Fijo ---
if strategy_params['use_dynamic_sl_tp']:
donchian_upper = signal_data['donchian_upper']
donchian_lower = signal_data['donchian_lower']
buffer_pips = strategy_params['sl_tp_buffer_pips']
risk_reward_ratio = strategy_params['risk_reward_ratio']
if order_type == "BUY":
# SL por debajo de Donchian lower + buffer
sl_calc = donchian_lower - (buffer_pips * point)
# Asegurarse de que el SL no esté demasiado cerca del precio actual
if current_price - sl_calc <
mt5.symbol_info(symbol).trade_stops_level * point: # Check against broker's minimum
stop level
sl = 0.0 # No set SL if too close
self._log(f"Advertencia: SL para {symbol} (BUY) demasiado cerca
del precio actual ({current_price:.5f}). No se estableció SL.")
else:
sl = sl_calc
# TP calculado en base al riesgo
risk_distance = current_price - sl # Esto será negativo si sl=0
if sl != 0.0 and risk_distance > 0: # Solo calcular TP si SL es
válido y la distancia es positiva
tp = current_price + (risk_distance * risk_reward_ratio)
else:
tp = 0.0
self._log(f"Advertencia: TP para {symbol} (BUY) no establecido
debido a SL inválido o distancia de riesgo cero/negativa.")
elif order_type == "SELL":
# SL por encima de Donchian upper + buffer
sl_calc = donchian_upper + (buffer_pips * point)
# Asegurarse de que el SL no esté demasiado cerca del precio actual
if sl_calc - current_price <
mt5.symbol_info(symbol).trade_stops_level * point:
sl = 0.0 # No set SL if too close
self._log(f"Advertencia: SL para {symbol} (SELL) demasiado
cerca del precio actual ({current_price:.5f}). No se estableció SL.")
else:
sl = sl_calc
# TP calculado en base al riesgo
risk_distance = sl - current_price # Esto será negativo si sl=0
if sl != 0.0 and risk_distance > 0: # Solo calcular TP si SL es
válido y la distancia es positiva
tp = current_price - (risk_distance * risk_reward_ratio)
else:
tp = 0.0
self._log(f"Advertencia: TP para {symbol} (SELL) no establecido
debido a SL inválido o distancia de riesgo cero/negativa.")
self._log(f"SL/TP Dinámico calculado para {symbol}: SL={sl:.5f},
TP={tp:.5f} (Riesgo/Beneficio: {risk_reward_ratio})")
else: # Usar SL/TP fijo en pips
sl_pips = strategy_params['sl_pips']
tp_pips = strategy_params['tp_pips']
# Obtener el nivel mínimo de stops del bróker
min_stops_level = mt5.symbol_info(symbol).trade_stops_level
if order_type == "BUY":
sl = current_price - sl_pips * point
tp = current_price + tp_pips * point
# Verificar si el SL fijo es demasiado cercano al precio de entrada
según el bróker
if sl_pips * point < min_stops_level * point: # Convertir pips a
valor en puntos para comparar
self._log(f"Advertencia: SL fijo ({sl_pips} pips) para {symbol}
(BUY) está por debajo del mínimo del bróker ({min_stops_level} puntos). No se
establecerá SL para evitar Retcode 10016.")
sl = 0.0
if tp_pips * point < min_stops_level * point:
self._log(f"Advertencia: TP fijo ({tp_pips} pips) para {symbol}
(BUY) está por debajo del mínimo del bróker ({min_stops_level} puntos). No se
establecerá TP para evitar Retcode 10016.")
tp = 0.0
elif order_type == "SELL":
sl = current_price + sl_pips * point
tp = current_price - tp_pips * point
# Verificar si el SL fijo es demasiado cercano al precio de entrada
según el bróker
if sl_pips * point < min_stops_level * point:
self._log(f"Advertencia: SL fijo ({sl_pips} pips) para {symbol}
(SELL) está por debajo del mínimo del bróker ({min_stops_level} puntos). No se
establecerá SL para evitar Retcode 10016.")
sl = 0.0
if tp_pips * point < min_stops_level * point:
self._log(f"Advertencia: TP fijo ({tp_pips} pips) para {symbol}
(SELL) está por debajo del mínimo del bróker ({min_stops_level} puntos). No se
establecerá TP para evitar Retcode 10016.")
tp = 0.0
self._log(f"SL/TP Fijo utilizado para {symbol}: SL={sl_pips} pips,
TP={tp_pips} pips.")
# Asegurar que SL/TP sean válidos y no crucen el precio
if order_type == "BUY":
if sl != 0.0 and sl >= current_price: sl = 0.0 # SL debe ser menor para
BUY
if tp != 0.0 and tp <= current_price: tp = 0.0 # TP debe ser mayor para
BUY
elif order_type == "SELL":
if sl != 0.0 and sl <= current_price: sl = 0.0 # SL debe ser mayor para
SELL
if tp != 0.0 and tp >= current_price: tp = 0.0 # TP debe ser menor para
SELL
# Determinar el tipo de orden (BUY o SELL)
mt5_order_type = mt5.ORDER_TYPE_BUY if order_type == "BUY" else
mt5.ORDER_TYPE_SELL
# Preparar la solicitud de la orden
request = {
"action": mt5.TRADE_ACTION_DEAL,
"symbol": symbol,
"volume": trade_volume,
"type": mt5_order_type,
"price": current_price, # Usar el precio actual del tick
"deviation": 20, # Desviación permitida en pips
"magic": 20240616, # ID mágico para identificar órdenes de tu bot
"comment": "STRD Bot Trade",
"type_time": mt5.ORDER_TIME_GTC, # Good Till Cancel
"type_filling": mt5.ORDER_FILLING_FOK, # Fill Or Kill
}
# Añadir SL y TP si son válidos (no 0.0)
if sl > 0:
request["sl"] = sl
if tp > 0:
request["tp"] = tp
self._log(f"Enviando orden: {order_type} {trade_volume} {symbol} @
{current_price:.5f} (SL: {sl:.5f}, TP: {tp:.5f})")
result = mt5.order_send(request)
if result:
if [Link] == mt5.TRADE_RETCODE_DONE:
position_ticket = [Link]
self.open_trades[position_ticket] = {
'symbol': symbol,
'direction': order_type,
'entry_time': [Link](),
'gui_item_id': gui_item_id
}
self._log(f"Orden '{order_type} {symbol}' enviada con éxito. Ticket
de ORDEN: {[Link]}, Ticket de POSICIÓN: {[Link]}, Precio:
{[Link]}, Volumen: {[Link]}")
# Actualizar la GUI con el ID de la posición como parte del estado
[Link](0, app.update_trade_result, gui_item_id, f"Abierta
(ID: {position_ticket})", "")
# Almacenar position_ticket como una etiqueta para el elemento de
la GUI
[Link](0, app.trade_table.item, gui_item_id,
tags=(position_ticket,))
return {
"status": "EXITO",
"order_id": [Link],
"position_id": [Link],
"price_executed": [Link],
"volume": [Link],
"comment": [Link],
"sl_set": sl,
"tp_set": tp
}
else:
self._log(f"Error al enviar orden: {[Link]} (Retcode:
{[Link]})")
self._log(f"Detalles del error: {mt5.last_error()}")
[Link](0, app.update_trade_result, gui_item_id, f"FALLO
(Retcode: {[Link]})", "")
return {
"status": "FALLO",
"comment": [Link],
"retcode": [Link],
"error_mt5": mt5.last_error()
}
else:
self._log(f"Error desconocido al enviar orden: {mt5.last_error()}")
[Link](0, app.update_trade_result, gui_item_id, "FALLO
(Desconocido)", "")
return {
"status": "FALLO",
"comment": "Error desconocido",
"error_mt5": mt5.last_error()
}
def close_position(self, position_ticket):
if not [Link]:
self._log("Error: MT5 no conectado. No se puede cerrar la posición.")
return False
# Obtener detalles de la posición
positions = mt5.positions_get(ticket=position_ticket)
if not positions:
self._log(f"Error: Posición {position_ticket} no encontrada o ya
cerrada en MT5.")
return False
position = positions[0] # Solo debería haber una posición con este ticket
# Determinar el tipo de orden opuesto para cerrar
close_order_type = mt5.ORDER_TYPE_SELL if [Link] ==
mt5.ORDER_TYPE_BUY else mt5.ORDER_TYPE_BUY
# Obtener precio actual para cerrar
tick = mt5.symbol_info_tick([Link])
if tick is None:
self._log(f"Error: No se pudo obtener el tick para cerrar el símbolo
{[Link]}.")
return False
close_price = [Link] if [Link] == mt5.ORDER_TYPE_BUY else [Link]
request = {
"action": mt5.TRADE_ACTION_DEAL,
"position": [Link], # El ticket de la posición a cerrar
"symbol": [Link],
"volume": [Link], # El volumen de la posición a cerrar
"type": close_order_type,
"price": close_price,
"deviation": 20,
"magic": 20240616, # Número mágico para las operaciones del bot
"comment": "Bot manual close",
"type_time": mt5.ORDER_TIME_GTC,
"type_filling": mt5.ORDER_FILLING_FOK,
}
self._log(f"Enviando solicitud para cerrar posición {[Link]} (ID:
{[Link]})...")
result = mt5.order_send(request)
if result and [Link] == mt5.TRADE_RETCODE_DONE:
self._log(f"Posición {[Link]} (ID: {[Link]}) cerrada
con éxito. Deal: {[Link]}.")
# El bucle run_market_data_loop detectará este cierre y actualizará la
GUI
return True
else:
self._log(f"Error al cerrar posición {[Link]} (ID:
{[Link]}): {[Link] if result else 'No result'} (Retcode:
{[Link] if result else 'N/A'}). Error MT5: {mt5.last_error()}")
return False
def _get_closed_trade_result(self, position_ticket):
# Buscar en el historial de deals por el ticket de posición
deals = mt5.history_deals_get(position=position_ticket)
if deals is None:
self._log(f"Error al obtener deals históricos para posición
{position_ticket}: {mt5.last_error()}")
return None
if not deals:
self._log(f"No se encontraron deals para la posición {position_ticket}
en el historial, puede que no esté cerrada aún o no haya sido procesada.")
return None
total_profit = 0.0
# Sumar el profit/loss de todos los deals asociados a esta posición
for deal in deals:
total_profit += [Link]
status_text = "GANANCIA" if total_profit >= 0 else "PÉRDIDA"
profit_text = f"{total_profit:.2f}" # Formatear la ganancia/pérdida
return {"status": status_text, "profit": profit_text}
def run_balance_update_loop(self, update_interval_seconds=1):
self._log(f"Iniciando bucle de actualización de balance (cada
{update_interval_seconds}s)...")
while not self.balance_shutdown_event.is_set():
if [Link]:
account_info = self.get_account_info()
if account_info:
[Link](0, app.update_account_display, account_info)
self.balance_shutdown_event.wait(update_interval_seconds)
self._log("Bucle de actualización de balance detenido.")
def run_market_data_loop(self, update_interval=5):
self._log("Iniciando bucle de datos de mercado y análisis de señales...")
last_candle_time = {}
while not self.shutdown_event.is_set():
if [Link]:
if not self.monitored_symbols:
self.monitored_symbols = SPECIFIC_SYMBOLS_TO_MONITOR
self._log(f"Configurando monitoreo para los siguientes
símbolos: {self.monitored_symbols}")
[Link](0, app.update_symbols_monitored_display,
len(self.monitored_symbols))
for s in self.monitored_symbols:
symbol_info = mt5.symbol_info(s)
if symbol_info is None:
self._log(f"Advertencia: Símbolo '{s}' no encontrado en
MT5. Asegúrate de que esté disponible en tu Market Watch y el nombre sea exacto.")
elif not symbol_info.visible:
if mt5.symbol_select(s, True):
self._log(f"Símbolo '{s}' agregado al Market
Watch.")
else:
self._log(f"Error: No se pudo agregar '{s}' al
Market Watch. Puede que no esté disponible para trading.")
# --- Verificación de operaciones cerradas y gestión de señales
pendientes ---
current_open_positions_from_mt5 = {}
mt5_positions = mt5.positions_get()
if mt5_positions is not None:
current_open_positions_from_mt5 = {[Link]: pos for pos in
mt5_positions}
else:
self._log(f"Error al obtener posiciones abiertas de MT5:
{mt5.last_error()}")
closed_tickets_to_process = []
for position_ticket, trade_info in list(self.open_trades.items()):
if position_ticket not in current_open_positions_from_mt5:
closed_tickets_to_process.append(position_ticket)
for position_ticket in closed_tickets_to_process:
self._log(f"Detectada operación cerrada (Position ID:
{position_ticket}). Obteniendo resultado...")
result_info = self._get_closed_trade_result(position_ticket)
account_info = self.get_account_info()
currency = account_info['currency'] if account_info else "USD"
if result_info:
gui_item_id = self.open_trades[position_ticket]
['gui_item_id']
final_result_text = f"{result_info['status']}
({result_info['profit']} {currency})"
[Link](0, app.update_trade_result, gui_item_id,
final_result_text, "")
self._log(f"Operación {position_ticket} finalizada:
{result_info['status']} con {result_info['profit']} {currency}.")
else:
self._log(f"No se pudo obtener el resultado para la
operación cerrada {position_ticket}. Posible error o aún no registrada.")
gui_item_id = self.open_trades[position_ticket]
['gui_item_id']
[Link](0, app.update_trade_result, gui_item_id,
"CERRADA (Resultado N/A)", "")
del self.open_trades[position_ticket] # Eliminar de las
operaciones rastreadas
# --- Procesar señales pendientes si hay espacio ---
while self.pending_signals and len(self.open_trades) <
MAX_SIMULTANEOUS_TRADES:
signal_data, gui_item_id = self.pending_signals.pop(0) #
Obtener la señal más antigua
### NUEVA LÓGICA PARA UN MISMO PAR (en cola de pendientes) ###
# Re-verificar si ya hay una operación abierta en este par
if any(trade['symbol'] == signal_data['symbol'] for trade in
self.open_trades.values()):
self._log(f"[{signal_data['symbol']}] (Pendiente) Ya existe
una operación abierta para este par. Descartando señal pendiente.")
[Link](0, app.update_trade_result, gui_item_id,
"DESCARTADA (Par Abierto)", "")
continue # Pasa a la siguiente señal pendiente
### FIN NUEVA LÓGICA ###
self._log(f"Procesando señal pendiente para
{signal_data['symbol']} {signal_data['direction']} (Hay {len(self.open_trades)}
operaciones abiertas, límite {MAX_SIMULTANEOUS_TRADES}).")
if self.auto_mode_var.get():
self._log(f"Modo automático: Ejecutando operación pendiente
para {signal_data['symbol']} {signal_data['direction']}...")
trade_result = self.send_order(signal_data, gui_item_id)
else: # Modo manual, pedir confirmación para la pendiente
self._log(f"Modo manual: Esperando confirmación para señal
pendiente {signal_data['symbol']} {signal_data['direction']}...")
[Link](0, app.show_trade_confirmation_popup,
signal_data, gui_item_id)
self.signal_event.clear()
if not self.signal_event.wait(timeout=60): # Esperar
confirmación del usuario
self._log(f"Tiempo de espera agotado para confirmación
de {signal_data['symbol']}. Señal pendiente rechazada automáticamente.")
self.signal_confirmed = False
if self.signal_confirmed:
self._log(f"Usuario confirmó la operación pendiente
para {signal_data['symbol']}. Enviando orden...")
trade_result = self.send_order(signal_data,
gui_item_id)
else:
self._log(f"Usuario rechazó la operación pendiente o
expiró para {signal_data['symbol']}.")
[Link](0, app.update_trade_result,
gui_item_id, "RECHAZADA/EXPIRADA", "")
# --- Inicio del bucle para escanear cada símbolo monitoreado ---
for symbol in self.monitored_symbols:
try:
# Mantener la actualización de precio para un símbolo por
defecto (ej. EURUSDm)
# o para el primer símbolo de tu lista monitoreada.
if symbol == DEFAULT_SYMBOL:
price = self.get_latest_price(DEFAULT_SYMBOL)
if price is not None:
[Link](0, app.update_price_display,
DEFAULT_SYMBOL, price)
params = self.get_strategy_params_callback()
if params is None:
continue
# Ajustar el número de velas a solicitar para incluir los
periodos de los indicadores
min_candles_for_fetch = max(
params['donchian_period'] + 5,
params['rsi_period'] + 15 if params['use_rsi'] else 0,
params['sma_slow_period'] + 5 if params['use_sma'] else
0,
params['volume_sma_period'] + 5 if
params['use_volume_confirmation'] else 0,
params['adx_period'] * 2 + 5 if params['use_adx'] else
0
)
ohlcv_df = self.fetch_ohlcv(symbol, DEFAULT_INTERVAL,
count=min_candles_for_fetch)
if ohlcv_df is not None and not ohlcv_df.empty:
current_candle_timestamp = ohlcv_df.index[-1]
if last_candle_time.get((symbol, DEFAULT_INTERVAL)) !=
current_candle_timestamp:
last_candle_time[(symbol, DEFAULT_INTERVAL)] =
current_candle_timestamp
self._log(f"Nueva vela cerrada para {symbol}
({DEFAULT_INTERVAL}). Analizando...")
signal_data =
self.generate_donchian_signal(ohlcv_df, symbol, DEFAULT_INTERVAL)
if signal_data:
self._log(f"¡Señal detectada para
{signal_data['symbol']} {signal_data['direction']}!")
### NUEVA LÓGICA PARA UN MISMO PAR ###
# Verificar si ya hay una operación abierta en
este par
if any(trade['symbol'] == signal_data['symbol']
for trade in self.open_trades.values()):
self._log(f"[{signal_data['symbol']}] Ya
existe una operación abierta para este par. Saltando nueva señal.")
# Notificar en la GUI que se descartó la
señal
gui_trade_data = (
[Link]().strftime('%H:%M:%S'),
signal_data['symbol'],
signal_data['strategy'],
signal_data['direction'],
"DEMO" if "DEMO" in
app.mt5_client.get_account_info()['server'] else "REAL",
"DESCARTADA (Par Abierto)", # Nuevo
estado
)
[Link](0, app.add_trade_to_table,
gui_trade_data)
continue # Salta esta señal.
### FIN NUEVA LÓGICA ###
initial_status = "Pendiente de Ejecución"
gui_trade_data = (
[Link]().strftime('%H:%M:%M'), # Usar
HH:MM:SS
signal_data['symbol'],
signal_data['strategy'],
signal_data['direction'],
"DEMO" if "DEMO" in
app.mt5_client.get_account_info()['server'] else "REAL",
initial_status,
)
gui_item_id =
app.add_trade_to_table(gui_trade_data)
if len(self.open_trades) >=
MAX_SIMULTANEOUS_TRADES:
self._log(f"Límite de operaciones
({MAX_SIMULTANEOUS_TRADES}) alcanzado. Señal para {signal_data['symbol']} añadida a
cola pendiente.")
[Link](0,
app.update_trade_result, gui_item_id, "PENDIENTE (Límite Alcanzado)", "")
self.pending_signals.append((signal_data,
gui_item_id))
continue # Skip sending order, just add to
queue
if self.auto_mode_var.get():
self._log(f"Modo automático: Ejecutando
operación para {signal_data['symbol']} {signal_data['direction']}...")
trade_result = self.send_order(signal_data,
gui_item_id)
else: # Modo manual
self._log(f"Modo manual: Esperando
confirmación para {signal_data['symbol']} {signal_data['direction']}...")
[Link](0,
app.show_trade_confirmation_popup, signal_data, gui_item_id)
self.signal_event.clear()
if not self.signal_event.wait(timeout=60):
# Esperar confirmación del usuario
self._log(f"Tiempo de espera agotado
para confirmación de {signal_data['symbol']}. Señal rechazada automáticamente.")
self.signal_confirmed = False
if self.signal_confirmed:
self._log(f"Usuario confirmó la
operación para {signal_data['symbol']}. Enviando orden...")
trade_result =
self.send_order(signal_data, gui_item_id)
else:
self._log(f"Usuario rechazó la
operación o expiró para {signal_data['symbol']}.")
[Link](0,
app.update_trade_result, gui_item_id, "RECHAZADA/EXPIRADA", "")
except Exception as e:
self._log(f"Error procesando símbolo {symbol}: {e}")
else:
self._log("MT5 no conectado. Esperando conexión...")
self.shutdown_event.wait(update_interval)
self._log("Bucle de datos de mercado y análisis de señales detenido.")
# --- INTERFAZ GRÁFICA (Tkinter) ---
class TradingApp:
def __init__(self, master):
[Link] = master
[Link]("Bot STRD - Trading Automatizado")
[Link]("800x850") # Ajustado para más espacio
[Link](True, True)
self.auto_mode_var = [Link](value=False)
self.trade_volume_var = [Link](value=0.01)
self.use_rsi_var = [Link](value=USE_RSI_CONFIRMATION)
self.rsi_period_var = [Link](value=RSI_PERIOD)
self.rsi_overbought_var = [Link](value=RSI_OVERBOUGHT)
self.rsi_oversold_var = [Link](value=RSI_OVERSOLD)
self.use_sma_var = [Link](value=USE_SMA_TREND_CONFIRMATION)
self.sma_fast_period_var = [Link](value=SMA_FAST_PERIOD) # Nueva
variable
self.sma_slow_period_var = [Link](value=SMA_SLOW_PERIOD) # Nueva
variable
# Variables para Volumen
self.use_volume_var = [Link](value=USE_VOLUME_CONFIRMATION)
self.volume_sma_period_var = [Link](value=VOLUME_SMA_PERIOD)
self.volume_multiplier_threshold_var =
[Link](value=VOLUME_MULTIPLIER_THRESHOLD)
# Variables para ADX
self.use_adx_var = [Link](value=USE_ADX_CONFIRMATION)
self.adx_period_var = [Link](value=ADX_PERIOD)
self.adx_threshold_var = [Link](value=ADX_THRESHOLD)
self.use_dynamic_sl_tp_var = [Link](value=USE_DYNAMIC_SL_TP)
self.sl_pips_var = [Link](value=DEFAULT_SL_PIPS)
self.tp_pips_var = [Link](value=DEFAULT_TP_PIPS)
self.sl_tp_buffer_pips_var = [Link](value=DYNAMIC_SL_TP_BUFFER_PIPS)
self.risk_reward_ratio_var = [Link](value=DYNAMIC_RISK_REWARD_RATIO)
self.max_simultaneous_trades_var = [Link](value=MAX_SIMULTANEOUS_TRADES)
# Nueva variable
self.remember_credentials_var = [Link](value=False)
[Link] = [Link]()
self.mt5_client = MT5Client(self.log_message, self.auto_mode_var,
self.get_current_strategy_params)
self.market_data_thread = None
self.balance_update_thread = None
self._create_widgets()
self._load_saved_credentials()
self.toggle_sl_tp_mode() # Llamar para establecer el estado inicial de los
campos SL/TP
def _create_widgets(self):
[Link].grid_rowconfigure(0, weight=1)
[Link].grid_columnconfigure(0, weight=1)
[Link] = [Link]([Link])
[Link](row=0, column=0, sticky="nsew")
[Link] = [Link]([Link], orient="vertical",
command=[Link])
[Link](row=0, column=1, sticky="ns")
[Link](yscrollcommand=[Link])
self.scrollable_frame = [Link]([Link])
self.canvas_frame_id = [Link].create_window((0, 0),
window=self.scrollable_frame, anchor="nw")
self.scrollable_frame.bind("<Configure>", self.on_frame_configure)
[Link]("<Configure>", self.on_canvas_configure)
[Link].bind_all("<MouseWheel>", self._on_mousewheel)
[Link].bind_all("<Button-4>", self._on_mousewheel)
[Link].bind_all("<Button-5>", self._on_mousewheel)
self.scrollable_frame.grid_columnconfigure(0, weight=1)
# Ajustar el peso de las filas para permitir el crecimiento de los frames
self.scrollable_frame.grid_rowconfigure(0, weight=0) # Connection frame
self.scrollable_frame.grid_rowconfigure(1, weight=0) # Mode/Volume frame
self.scrollable_frame.grid_rowconfigure(2, weight=0) # Strategy frame
self.scrollable_frame.grid_rowconfigure(3, weight=0) # Limit frame
self.scrollable_frame.grid_rowconfigure(4, weight=0) # Info frame
self.scrollable_frame.grid_rowconfigure(5, weight=1) # Trade frame (debe
expandirse)
self.scrollable_frame.grid_rowconfigure(6, weight=1) # Log frame (debe
expandirse)
# --- Frame de Conexión MT5 ---
connection_frame = [Link](self.scrollable_frame, text="Configuración
de Conexión MT5", bd=2, relief="groove")
connection_frame.grid(row=0, column=0, padx=5, pady=5, sticky="ew")
[Link](connection_frame, text="MT5 Login:").grid(row=0, column=0, padx=5,
pady=2, sticky="w")
self.login_entry = [Link](connection_frame)
self.login_entry.insert(0, str(DEFAULT_MT5_LOGIN))
self.login_entry.grid(row=0, column=1, padx=5, pady=2, sticky="ew")
[Link](connection_frame, text="Contraseña:").grid(row=1, column=0,
padx=5, pady=2, sticky="w")
self.password_entry = [Link](connection_frame, show="*")
self.password_entry.insert(0, DEFAULT_MT5_PASSWORD)
self.password_entry.grid(row=1, column=1, padx=5, pady=2, sticky="ew")
[Link](connection_frame, text="Servidor MT5:").grid(row=2, column=0,
padx=5, pady=2, sticky="w")
self.server_combobox = [Link](connection_frame,
values=EXNESS_SERVERS)
self.server_combobox.set(DEFAULT_MT5_SERVER)
self.server_combobox.grid(row=2, column=1, padx=5, pady=2, sticky="ew")
self.remember_credentials_checkbox = [Link](
connection_frame,
text="Recordar ID y Contraseña",
variable=self.remember_credentials_var
)
self.remember_credentials_checkbox.grid(row=3, column=0, columnspan=2,
padx=5, pady=2, sticky="w")
self.connect_button = [Link](connection_frame, text="Conectar MT5",
command=self.connect_mt5_from_gui)
self.connect_button.grid(row=0, column=2, rowspan=3, padx=10, pady=5,
sticky="ns")
self.disconnect_button = [Link](connection_frame, text="Desconectar
MT5", command=self.disconnect_mt5, state=[Link])
self.disconnect_button.grid(row=3, column=2, padx=10, pady=5, sticky="ew")
connection_frame.grid_columnconfigure(1, weight=1)
# --- Frame de Modo de Operación y Volumen ---
mode_volume_frame = [Link](self.scrollable_frame, text="Modo de
Operación y Volumen", bd=2, relief="groove")
mode_volume_frame.grid(row=1, column=0, padx=5, pady=5, sticky="ew")
self.auto_mode_checkbox = [Link](
mode_volume_frame,
text="Modo Automático (Ejecutar operaciones sin confirmación)",
variable=self.auto_mode_var,
command=self.toggle_auto_mode
)
self.auto_mode_checkbox.grid(row=0, column=0, columnspan=2, padx=10,
pady=5, sticky="w")
[Link](mode_volume_frame, text="Volumen (Lotaje):").grid(row=1, column=0,
padx=10, pady=5, sticky="w")
self.volume_entry = [Link](mode_volume_frame,
textvariable=self.trade_volume_var, width=10)
self.volume_entry.grid(row=1, column=1, padx=10, pady=5, sticky="w")
mode_volume_frame.grid_columnconfigure(1, weight=1)
# --- Frame de Configuración de Estrategia ---
strategy_frame = [Link](self.scrollable_frame, text="Configuración
de Estrategia (Donchian + Confirmaciones)", bd=2, relief="groove")
strategy_frame.grid(row=2, column=0, padx=5, pady=5, sticky="ew")
[Link](strategy_frame, text="Período Donchian:").grid(row=0, column=0,
padx=5, pady=2, sticky="w")
[Link](strategy_frame, text=str(DONCHIAN_PERIOD), font=("Arial", 9,
"bold")).grid(row=0, column=1, padx=5, pady=2, sticky="w")
# RSI Controls
self.rsi_checkbox = [Link](strategy_frame, text="Usar Confirmación
RSI", variable=self.use_rsi_var)
self.rsi_checkbox.grid(row=1, column=0, padx=5, pady=2, sticky="w")
[Link](strategy_frame, text="Período RSI:").grid(row=2, column=0, padx=5,
pady=2, sticky="w")
self.rsi_period_entry = [Link](strategy_frame,
textvariable=self.rsi_period_var, width=5)
self.rsi_period_entry.grid(row=2, column=1, padx=5, pady=2, sticky="w")
[Link](strategy_frame, text="RSI Sobrecompra (ej. 70):").grid(row=3,
column=0, padx=5, pady=2, sticky="w")
self.rsi_overbought_entry = [Link](strategy_frame,
textvariable=self.rsi_overbought_var, width=5)
self.rsi_overbought_entry.grid(row=3, column=1, padx=5, pady=2, sticky="w")
[Link](strategy_frame, text="RSI Sobrevendido (ej. 30):").grid(row=4,
column=0, padx=5, pady=2, sticky="w")
self.rsi_oversold_entry = [Link](strategy_frame,
textvariable=self.rsi_oversold_var, width=5)
self.rsi_oversold_entry.grid(row=4, column=1, padx=5, pady=2, sticky="w")
# SMA Controls
self.sma_checkbox = [Link](strategy_frame, text="Usar Confirmación
SMA (Cruce Lenta/Rápida)", variable=self.use_sma_var)
self.sma_checkbox.grid(row=1, column=2, padx=5, pady=2, sticky="w")
[Link](strategy_frame, text="Período SMA Rápida (ej. 20):").grid(row=2,
column=2, padx=5, pady=2, sticky="w")
self.sma_fast_period_entry = [Link](strategy_frame,
textvariable=self.sma_fast_period_var, width=5)
self.sma_fast_period_entry.grid(row=2, column=3, padx=5, pady=2,
sticky="w")
[Link](strategy_frame, text="Período SMA Lenta (ej. 50):").grid(row=3,
column=2, padx=5, pady=2, sticky="w")
self.sma_slow_period_entry = [Link](strategy_frame,
textvariable=self.sma_slow_period_var, width=5)
self.sma_slow_period_entry.grid(row=3, column=3, padx=5, pady=2,
sticky="w")
# Volumen Controls (NUEVOS CONTROLES DE VOLUMEN)
self.volume_checkbox = [Link](strategy_frame, text="Usar
Confirmación Volumen", variable=self.use_volume_var)
self.volume_checkbox.grid(row=5, column=0, padx=5, pady=2, sticky="w")
[Link](strategy_frame, text="Período VMA:").grid(row=6, column=0, padx=5,
pady=2, sticky="w")
self.volume_sma_period_entry = [Link](strategy_frame,
textvariable=self.volume_sma_period_var, width=5)
self.volume_sma_period_entry.grid(row=6, column=1, padx=5, pady=2,
sticky="w")
[Link](strategy_frame, text="Multiplicador Volumen:").grid(row=7,
column=0, padx=5, pady=2, sticky="w")
self.volume_multiplier_threshold_entry = [Link](strategy_frame,
textvariable=self.volume_multiplier_threshold_var, width=5)
self.volume_multiplier_threshold_entry.grid(row=7, column=1, padx=5,
pady=2, sticky="w")
# ADX Controls (NUEVOS CONTROLES DE ADX)
self.adx_checkbox = [Link](strategy_frame, text="Usar Confirmación
ADX", variable=self.use_adx_var)
self.adx_checkbox.grid(row=5, column=2, padx=5, pady=2, sticky="w")
[Link](strategy_frame, text="Período ADX:").grid(row=6, column=2, padx=5,
pady=2, sticky="w")
self.adx_period_entry = [Link](strategy_frame,
textvariable=self.adx_period_var, width=5)
self.adx_period_entry.grid(row=6, column=3, padx=5, pady=2, sticky="w")
[Link](strategy_frame, text="Umbral ADX (ej. 25):").grid(row=7, column=2,
padx=5, pady=2, sticky="w")
self.adx_threshold_entry = [Link](strategy_frame,
textvariable=self.adx_threshold_var, width=5)
self.adx_threshold_entry.grid(row=7, column=3, padx=5, pady=2, sticky="w")
# SL/TP Controls (Posición ajustada)
self.dynamic_sl_tp_checkbox = [Link](
strategy_frame,
text="Usar SL/TP Dinámico (Basado en Donchian y Ratio)",
variable=self.use_dynamic_sl_tp_var,
command=self.toggle_sl_tp_mode
)
self.dynamic_sl_tp_checkbox.grid(row=8, column=0, columnspan=2, padx=5,
pady=5, sticky="w")
[Link](strategy_frame, text="SL Pips (Fijo):").grid(row=9, column=0,
padx=5, pady=2, sticky="w")
self.sl_pips_entry = [Link](strategy_frame,
textvariable=self.sl_pips_var, width=5)
self.sl_pips_entry.grid(row=9, column=1, padx=5, pady=2, sticky="w")
[Link](strategy_frame, text="TP Pips (Fijo):").grid(row=10, column=0,
padx=5, pady=2, sticky="w")
self.tp_pips_entry = [Link](strategy_frame,
textvariable=self.tp_pips_var, width=5)
self.tp_pips_entry.grid(row=10, column=1, padx=5, pady=2, sticky="w")
[Link](strategy_frame, text="Buffer SL (Pips Dinámico):").grid(row=9,
column=2, padx=5, pady=2, sticky="w")
self.sl_tp_buffer_pips_entry = [Link](strategy_frame,
textvariable=self.sl_tp_buffer_pips_var, width=5)
self.sl_tp_buffer_pips_entry.grid(row=9, column=3, padx=5, pady=2,
sticky="w")
[Link](strategy_frame, text="Ratio Riesgo/Beneficio
(Dinámico):").grid(row=10, column=2, padx=5, pady=2, sticky="w")
self.risk_reward_ratio_entry = [Link](strategy_frame,
textvariable=self.risk_reward_ratio_var, width=5)
self.risk_reward_ratio_entry.grid(row=10, column=3, padx=5, pady=2,
sticky="w")
strategy_frame.grid_columnconfigure(1, weight=1)
strategy_frame.grid_columnconfigure(3, weight=1)
# --- Nuevo Frame: Límite de Operaciones ---
limit_frame = [Link](self.scrollable_frame, text="Límite de
Operaciones", bd=2, relief="groove")
limit_frame.grid(row=3, column=0, padx=5, pady=5, sticky="ew")
[Link](limit_frame, text="Máx. Operaciones Simultáneas:").grid(row=0,
column=0, padx=5, pady=2, sticky="w")
self.max_simultaneous_trades_entry = [Link](limit_frame,
textvariable=self.max_simultaneous_trades_var, width=5)
self.max_simultaneous_trades_entry.grid(row=0, column=1, padx=5, pady=2,
sticky="w")
limit_frame.grid_columnconfigure(1, weight=1)
# --- Frame de Información de Cuenta y Mercado ---
info_frame = [Link](self.scrollable_frame, text="Información de
Cuenta y Mercado", bd=2, relief="groove")
info_frame.grid(row=4, column=0, padx=5, pady=5, sticky="ew") # Cambiado a
row 4
[Link](info_frame, text="Estado MT5:").grid(row=0, column=0, padx=5,
pady=2, sticky="w")
self.mt5_status_label = [Link](info_frame, text="Desconectado", fg="red")
self.mt5_status_label.grid(row=0, column=1, padx=5, pady=2, sticky="w")
[Link](info_frame, text="Balance:").grid(row=1, column=0, padx=5, pady=2,
sticky="w")
self.balance_label = [Link](info_frame, text="N/A", font=("Arial", 10,
"bold"))
self.balance_label.grid(row=1, column=1, padx=5, pady=2, sticky="w")
[Link](info_frame, text="Servidor Conectado:").grid(row=0, column=2,
padx=5, pady=2, sticky="w")
self.connected_server_label = [Link](info_frame, text="N/A")
self.connected_server_label.grid(row=0, column=3, padx=5, pady=2,
sticky="w")
[Link](info_frame, text="Símbolos Monitoreando:").grid(row=2, column=0,
padx=5, pady=2, sticky="w")
self.symbols_monitored_label = [Link](info_frame, text="0")
self.symbols_monitored_label.grid(row=2, column=1, padx=5, pady=2,
sticky="w")
[Link](info_frame, text=f"Precio {DEFAULT_SYMBOL}:").grid(row=3,
column=0, padx=5, pady=2, sticky="w")
self.price_label = [Link](info_frame, text="N/A", font=("Arial", 10,
"bold"))
self.price_label.grid(row=3, column=1, padx=5, pady=2, sticky="w")
[Link](info_frame, text="Operaciones Abiertas:").grid(row=2, column=2,
padx=5, pady=2, sticky="w")
self.open_trades_count_label = [Link](info_frame, text="0",
font=("Arial", 10, "bold"))
self.open_trades_count_label.grid(row=2, column=3, padx=5, pady=2,
sticky="w")
info_frame.grid_columnconfigure(1, weight=1)
info_frame.grid_columnconfigure(3, weight=1)
# --- Frame de Historial de Operaciones ---
trade_frame = [Link](self.scrollable_frame, text="Historial de
Operaciones", bd=2, relief="groove")
trade_frame.grid(row=5, column=0, padx=5, pady=5, sticky="nsew") # Cambiado
a row 5
self.trade_table = [Link](trade_frame, columns=("Hora", "Paridad",
"Estrategia", "Direccion", "Cuenta", "Resultado"), show="headings")
self.trade_table.heading("Hora", text="Hora")
self.trade_table.heading("Paridad", text="Paridad")
self.trade_table.heading("Estrategia", text="Estrategia")
self.trade_table.heading("Direccion", text="Direccion")
self.trade_table.heading("Cuenta", text="Cuenta")
self.trade_table.heading("Resultado", text="Resultado")
self.trade_table.column("Hora", width=100, anchor="center", stretch=[Link])
self.trade_table.column("Paridad", width=80, anchor="center",
stretch=[Link])
self.trade_table.column("Estrategia", width=120, anchor="center",
stretch=[Link])
self.trade_table.column("Direccion", width=70, anchor="center",
stretch=[Link])
self.trade_table.column("Cuenta", width=60, anchor="center", stretch=[Link])
self.trade_table.column("Resultado", width=200, anchor="center",
stretch=[Link])
self.trade_table.pack(side=[Link], fill=[Link], expand=True)
scrollbar = [Link](trade_frame, orient="vertical",
command=self.trade_table.yview)
[Link](side=[Link], fill="y")
self.trade_table.configure(yscrollcommand=[Link])
self.close_selected_trade_button = [Link](trade_frame, text="Cerrar
Operación Seleccionada", command=self.close_selected_trade, state=[Link],
bg="orange", fg="white", font=("Arial", 10, "bold"))
self.close_selected_trade_button.pack(pady=5)
self.trade_table.bind("<<TreeviewSelect>>", self.on_trade_table_select)
# --- Frame de Log ---
log_frame = [Link](self.scrollable_frame, text="Log de la
Aplicación", bd=2, relief="groove")
log_frame.grid(row=6, column=0, padx=5, pady=5, sticky="nsew") # Cambiado a
row 6
self.log_text = [Link](log_frame, height=8,
state='disabled', wrap=[Link])
self.log_text.pack(fill=[Link], expand=True)
def on_frame_configure(self, event):
"""Ajusta la región de desplazamiento del canvas cuando el frame cambia de
tamaño."""
[Link](scrollregion=[Link]("all"))
def on_canvas_configure(self, event):
"""Ajusta el ancho del frame dentro del canvas cuando el canvas cambia de
tamaño."""
[Link](self.canvas_frame_id, width=[Link])
def _on_mousewheel(self, event):
"""Maneja el desplazamiento con la rueda del ratón."""
if [Link] == 5 or [Link] == -120:
[Link].yview_scroll(1, "unit")
elif [Link] == 4 or [Link] == 120:
[Link].yview_scroll(-1, "unit")
def _load_saved_credentials(self):
"""Carga las credenciales de MT5 guardadas desde un archivo."""
if [Link](CREDENTIALS_FILE):
[Link](CREDENTIALS_FILE)
if 'MT5_CREDENTIALS' in [Link]:
try:
login = [Link]['MT5_CREDENTIALS'].get('login')
password = [Link]['MT5_CREDENTIALS'].get('password')
server = [Link]['MT5_CREDENTIALS'].get('server')
remember =
[Link]['MT5_CREDENTIALS'].getboolean('remember_me')
if login:
self.login_entry.delete(0, [Link])
self.login_entry.insert(0, login)
if password:
self.password_entry.delete(0, [Link])
self.password_entry.insert(0, password)
if server and server not in EXNESS_SERVERS:
current_values = list(self.server_combobox['values'])
if server not in current_values:
current_values.append(server)
self.server_combobox['values'] = current_values
self.server_combobox.set(server)
elif server:
self.server_combobox.set(server)
else:
self.server_combobox.set(DEFAULT_MT5_SERVER)
self.remember_credentials_var.set(remember)
self.log_message("Credenciales cargadas desde el archivo de
configuración.")
except Exception as e:
self.log_message(f"Error al cargar credenciales: {e}")
else:
self.log_message("Archivo de credenciales no encontrado. Iniciando con
valores por defecto.")
def _save_credentials(self, login, password, server, remember_me):
"""Guarda las credenciales de MT5 en un archivo."""
[Link]['MT5_CREDENTIALS'] = {
'login': str(login),
'password': password,
'server': server,
'remember_me': str(remember_me)
}
try:
with open(CREDENTIALS_FILE, 'w') as configfile:
[Link](configfile)
self.log_message("Credenciales guardadas con éxito.")
except Exception as e:
self.log_message(f"Error al guardar credenciales: {e}")
def get_current_strategy_params(self):
"""Obtiene los parámetros de la estrategia actuales desde la GUI."""
try:
volume = self.trade_volume_var.get()
if volume <= 0:
raise ValueError("El volumen de operación debe ser un número
positivo.")
rsi_period = self.rsi_period_var.get()
if rsi_period < 2:
raise ValueError("El período RSI debe ser mayor o igual a 2.")
rsi_overbought = self.rsi_overbought_var.get()
rsi_oversold = self.rsi_oversold_var.get()
if not (0 <= rsi_oversold < rsi_overbought <= 100):
raise ValueError("RSI Sobrecompra y Sobrevendido deben estar entre
0-100, y Sobrevendido < Sobrecompra.")
sma_fast_period = self.sma_fast_period_var.get()
sma_slow_period = self.sma_slow_period_var.get()
if sma_fast_period < 1 or sma_slow_period < 1:
raise ValueError("Los períodos SMA deben ser mayores o iguales a
1.")
if sma_fast_period >= sma_slow_period:
raise ValueError("El período SMA Rápida debe ser menor que el
período SMA Lenta.")
# Parámetros de Volumen
use_volume_confirmation = self.use_volume_var.get()
volume_sma_period = self.volume_sma_period_var.get()
volume_multiplier_threshold =
self.volume_multiplier_threshold_var.get()
if use_volume_confirmation and (volume_sma_period < 1 or
volume_multiplier_threshold <= 0):
raise ValueError("El período VMA debe ser mayor o igual a 1 y el
multiplicador de volumen mayor a 0.")
# Parámetros de ADX
use_adx_confirmation = self.use_adx_var.get()
adx_period = self.adx_period_var.get()
adx_threshold = self.adx_threshold_var.get()
if use_adx_confirmation and (adx_period < 1 or adx_threshold < 0):
raise ValueError("El período ADX debe ser mayor o igual a 1 y el
umbral ADX no negativo.")
use_dynamic_sl_tp = self.use_dynamic_sl_tp_var.get()
sl_pips = self.sl_pips_var.get()
tp_pips = self.tp_pips_var.get()
sl_tp_buffer_pips = self.sl_tp_buffer_pips_var.get()
risk_reward_ratio = self.risk_reward_ratio_var.get()
if not use_dynamic_sl_tp:
if sl_pips < 0:
raise ValueError("Los pips de SL (fijo) no pueden ser
negativos.")
if tp_pips < 0:
raise ValueError("Los pips de TP (fijo) no pueden ser
negativos.")
else:
if sl_tp_buffer_pips < 0:
raise ValueError("Los pips de Buffer SL (dinámico) no pueden
ser negativos.")
if risk_reward_ratio <= 0:
raise ValueError("El Ratio Riesgo/Beneficio (dinámico) debe ser
mayor que cero.")
max_simultaneous_trades = self.max_simultaneous_trades_var.get()
if max_simultaneous_trades <= 0:
raise ValueError("El número máximo de operaciones simultáneas debe
ser mayor a cero.")
return {
"trade_volume": volume,
"donchian_period": DONCHIAN_PERIOD,
"use_rsi": self.use_rsi_var.get(),
"rsi_period": rsi_period,
"rsi_overbought": rsi_overbought,
"rsi_oversold": rsi_oversold,
"use_sma": self.use_sma_var.get(),
"sma_fast_period": sma_fast_period,
"sma_slow_period": sma_slow_period,
"use_volume_confirmation": use_volume_confirmation,
"volume_sma_period": volume_sma_period,
"volume_multiplier_threshold": volume_multiplier_threshold,
"use_adx": use_adx_confirmation,
"adx_period": adx_period,
"adx_threshold": adx_threshold,
"use_dynamic_sl_tp": use_dynamic_sl_tp,
"sl_pips": sl_pips,
"tp_pips": tp_pips,
"sl_tp_buffer_pips": sl_tp_buffer_pips,
"risk_reward_ratio": risk_reward_ratio,
"max_simultaneous_trades": max_simultaneous_trades
}
except ValueError as e:
[Link]("Error de Configuración de Estrategia", str(e))
self.log_message(f"Error en parámetros de estrategia: {e}")
return None
def log_message(self, message):
"""Añade un mensaje al log de la aplicación."""
self.log_text.config(state='normal')
self.log_text.insert([Link], f"[{[Link]().strftime('%H:%M:%S')}]
{message}\n")
self.log_text.see([Link])
self.log_text.config(state='disabled')
def connect_mt5_from_gui(self):
"""Intenta conectar a MetaTrader 5 desde la interfaz gráfica."""
login_str = self.login_entry.get()
password = self.password_entry.get()
server = self.server_combobox.get()
if not login_str or not password or not server:
[Link]("Error de Conexión", "Por favor, introduce el
Login, Contraseña y Servidor de MT5.")
return
try:
login = int(login_str)
except ValueError:
[Link]("Error de Conexión", "El Login de MT5 debe ser un
número entero.")
return
strategy_params = self.get_current_strategy_params()
if strategy_params is None:
return
if self.remember_credentials_var.get():
self._save_credentials(login, password, server, True)
else:
if [Link](CREDENTIALS_FILE):
try:
[Link](CREDENTIALS_FILE)
self.log_message("Archivo de credenciales eliminado (opción
'Recordar' desmarcada).")
except Exception as e:
self.log_message(f"Error al eliminar archivo de credenciales:
{e}")
self.log_message("Intentando conectar a MT5...")
self.mt5_status_label.config(text="Conectando...", fg="orange")
self.connect_button.config(state=[Link])
self.disconnect_button.config(state=[Link])
# Deshabilitar configuración de entrada durante la conexión y operación
para evitar cambios en vivo
self.login_entry.config(state=[Link])
self.password_entry.config(state=[Link])
self.server_combobox.config(state=[Link])
self.remember_credentials_checkbox.config(state=[Link])
self.volume_entry.config(state=[Link])
self.rsi_checkbox.config(state=[Link])
self.rsi_period_entry.config(state=[Link])
self.rsi_overbought_entry.config(state=[Link])
self.rsi_oversold_entry.config(state=[Link])
self.sma_checkbox.config(state=[Link])
self.sma_fast_period_entry.config(state=[Link]) # Deshabilitar
self.sma_slow_period_entry.config(state=[Link]) # Deshabilitar
# Deshabilitar los nuevos controles de volumen y ADX al conectar
self.volume_checkbox.config(state=[Link])
self.volume_sma_period_entry.config(state=[Link])
self.volume_multiplier_threshold_entry.config(state=[Link])
self.adx_checkbox.config(state=[Link])
self.adx_period_entry.config(state=[Link])
self.adx_threshold_entry.config(state=[Link])
self.sl_pips_entry.config(state=[Link])
self.tp_pips_entry.config(state=[Link])
self.dynamic_sl_tp_checkbox.config(state=[Link])
self.sl_tp_buffer_pips_entry.config(state=[Link])
self.risk_reward_ratio_entry.config(state=[Link])
self.max_simultaneous_trades_entry.config(state=[Link]) # Deshabilitar
def run_connection():
"""Función para ejecutar la conexión a MT5 en un hilo separado."""
if self.mt5_client.connect_mt5(login, password, server):
[Link](0, self._on_mt5_connected)
else:
[Link](0, self._on_mt5_connection_failed)
connection_thread = [Link](target=run_connection)
connection_thread.daemon = True
connection_thread.start()
def _on_mt5_connected(self):
"""Callback cuando MT5 se conecta exitosamente."""
self.mt5_status_label.config(text="Conectado", fg="green")
account_info = self.mt5_client.get_account_info()
if account_info:
self.connected_server_label.config(text=account_info['server'],
fg="blue")
else:
self.connected_server_label.config(text="Desconocido", fg="orange")
self.disconnect_button.config(state=[Link])
self.log_message("Conexión a MT5 establecida. Iniciando monitoreo de datos
de mercado.")
self.mt5_client.balance_shutdown_event.clear()
self.balance_update_thread =
[Link](target=self.mt5_client.run_balance_update_loop, args=(1,))
self.balance_update_thread.daemon = True
self.balance_update_thread.start()
self.mt5_client.shutdown_event.clear()
self.market_data_thread =
[Link](target=self.mt5_client.run_market_data_loop, args=(5,))
self.market_data_thread.daemon = True
self.market_data_thread.start()
self.auto_mode_checkbox.config(state=[Link])
self.toggle_sl_tp_mode(True) # Re-habilita los SL/TP relacionados a la
estrategia
# El resto de los campos de estrategia deberían permanecer deshabilitados
si no se pueden cambiar en caliente
self.volume_entry.config(state=[Link]) # Este sí se puede cambiar en
caliente
def _on_mt5_connection_failed(self):
"""Callback cuando la conexión a MT5 falla."""
self.mt5_status_label.config(text="Falló", fg="red")
self.connected_server_label.config(text="N/A", fg="red")
self.connect_button.config(state=[Link])
self.disconnect_button.config(state=[Link])
self.log_message("Error al conectar a MT5. Verifica tus credenciales y que
MT5 esté abierto.")
# Re-habilitar todos los campos de configuración si la conexión falla
self.login_entry.config(state=[Link])
self.password_entry.config(state=[Link])
self.server_combobox.config(state=[Link])
self.remember_credentials_checkbox.config(state=[Link])
self.volume_entry.config(state=[Link])
self.rsi_checkbox.config(state=[Link])
self.rsi_period_entry.config(state=[Link])
self.rsi_overbought_entry.config(state=[Link])
self.rsi_oversold_entry.config(state=[Link])
self.sma_checkbox.config(state=[Link])
self.sma_fast_period_entry.config(state=[Link])
self.sma_slow_period_entry.config(state=[Link])
# Re-habilitar los nuevos controles de volumen y ADX
self.volume_checkbox.config(state=[Link])
self.volume_sma_period_entry.config(state=[Link])
self.volume_multiplier_threshold_entry.config(state=[Link])
self.adx_checkbox.config(state=[Link])
self.adx_period_entry.config(state=[Link])
self.adx_threshold_entry.config(state=[Link])
self.sl_pips_entry.config(state=[Link])
self.tp_pips_entry.config(state=[Link])
self.dynamic_sl_tp_checkbox.config(state=[Link])
self.sl_tp_buffer_pips_entry.config(state=[Link])
self.risk_reward_ratio_entry.config(state=[Link])
self.auto_mode_checkbox.config(state=[Link])
self.max_simultaneous_trades_entry.config(state=[Link])
def disconnect_mt5(self):
"""Desconecta la aplicación de MetaTrader 5 y detiene los bucles."""
self.log_message("Desconectando MT5...")
self.mt5_client.shutdown_event.set()
self.mt5_client.balance_shutdown_event.set()
if self.market_data_thread and self.market_data_thread.is_alive():
self.market_data_thread.join(timeout=5)
if self.market_data_thread.is_alive():
self.log_message("Advertencia: El hilo de datos de mercado no
terminó a tiempo.")
if self.balance_update_thread and self.balance_update_thread.is_alive():
self.balance_update_thread.join(timeout=2)
if self.balance_update_thread.is_alive():
self.log_message("Advertencia: El hilo de balance no terminó a
tiempo.")
self.mt5_client.disconnect_mt5()
self.mt5_status_label.config(text="Desconectado", fg="red")
self.connected_server_label.config(text="N/A", fg="red")
self.balance_label.config(text="N/A")
self.price_label.config(text="N/A")
self.symbols_monitored_label.config(text="0")
self.open_trades_count_label.config(text="0") # Resetear contador de
operaciones
self.connect_button.config(state=[Link])
self.disconnect_button.config(state=[Link])
self.close_selected_trade_button.config(state=[Link])
# Habilitar todos los campos de configuración al desconectar
self.login_entry.config(state=[Link])
self.password_entry.config(state=[Link])
self.server_combobox.config(state=[Link])
self.remember_credentials_checkbox.config(state=[Link])
self.volume_entry.config(state=[Link])
self.rsi_checkbox.config(state=[Link])
self.rsi_period_entry.config(state=[Link])
self.rsi_overbought_entry.config(state=[Link])
self.rsi_oversold_entry.config(state=[Link])
self.sma_checkbox.config(state=[Link])
self.sma_fast_period_entry.config(state=[Link])
self.sma_slow_period_entry.config(state=[Link])
# Habilitar los nuevos controles de volumen y ADX
self.volume_checkbox.config(state=[Link])
self.volume_sma_period_entry.config(state=[Link])
self.volume_multiplier_threshold_entry.config(state=[Link])
self.adx_checkbox.config(state=[Link])
self.adx_period_entry.config(state=[Link])
self.adx_threshold_entry.config(state=[Link])
self.sl_pips_entry.config(state=[Link])
self.tp_pips_entry.config(state=[Link])
self.dynamic_sl_tp_checkbox.config(state=[Link])
self.sl_tp_buffer_pips_entry.config(state=[Link])
self.risk_reward_ratio_entry.config(state=[Link])
self.max_simultaneous_trades_entry.config(state=[Link])
self.auto_mode_checkbox.config(state=[Link])
self.log_message("MT5 desconectado exitosamente.")
def toggle_sl_tp_mode(self, force_enable_all=False):
"""Alterna la visibilidad y estado de los campos de SL/TP según el modo
dinámico/fijo."""
# Esta función ahora también se llama desde _on_mt5_connected, para
asegurar el estado correcto
# de los controles de SL/TP una vez conectado.
if self.mt5_client.connected: # Solo habilitar si hay conexión MT5 para
permitir cambios en caliente
# Habilitar los checkboxes para que el usuario pueda cambiar de modo
self.dynamic_sl_tp_checkbox.config(state=[Link])
self.max_simultaneous_trades_entry.config(state=[Link]) # Habilitar
la entrada del límite
if self.use_dynamic_sl_tp_var.get():
self.sl_pips_entry.config(state=[Link])
self.tp_pips_entry.config(state=[Link])
self.sl_tp_buffer_pips_entry.config(state=[Link])
self.risk_reward_ratio_entry.config(state=[Link])
self.log_message("Modo SL/TP: Dinámico (basado en Donchian y
Ratio).")
else:
self.sl_pips_entry.config(state=[Link])
self.tp_pips_entry.config(state=[Link])
self.sl_tp_buffer_pips_entry.config(state=[Link])
self.risk_reward_ratio_entry.config(state=[Link])
self.log_message("Modo SL/TP: Fijo (en pips).")
# Las otras configuraciones de estrategia (RSI/SMA periods, Volumen,
ADX) se mantienen disabled si se conectó.
# Solo se habilitan cuando está desconectado para evitar cambios
durante la operación.
self.volume_entry.config(state=[Link]) # Este sí se puede cambiar en
caliente
else:
# Si no hay conexión MT5, habilitar todo para que el usuario pueda
configurar antes de conectar
self.sl_pips_entry.config(state=[Link])
self.tp_pips_entry.config(state=[Link])
self.dynamic_sl_tp_checkbox.config(state=[Link])
self.sl_tp_buffer_pips_entry.config(state=[Link])
self.risk_reward_ratio_entry.config(state=[Link])
self.max_simultaneous_trades_entry.config(state=[Link])
self.volume_entry.config(state=[Link])
self.rsi_checkbox.config(state=[Link])
self.rsi_period_entry.config(state=[Link])
self.rsi_overbought_entry.config(state=[Link])
self.rsi_oversold_entry.config(state=[Link])
self.sma_checkbox.config(state=[Link])
self.sma_fast_period_entry.config(state=[Link])
self.sma_slow_period_entry.config(state=[Link])
# Habilitar los nuevos controles de volumen y ADX
self.volume_checkbox.config(state=[Link])
self.volume_sma_period_entry.config(state=[Link])
self.volume_multiplier_threshold_entry.config(state=[Link])
self.adx_checkbox.config(state=[Link])
self.adx_period_entry.config(state=[Link])
self.adx_threshold_entry.config(state=[Link])
self.log_message("Modo SL/TP: Desconectado. Ajusta la configuración
para la próxima conexión.")
def update_account_display(self, account_info):
"""Actualiza la información de la cuenta en la GUI."""
self.balance_label.config(text=f"{account_info['balance']:.2f}
{account_info['currency']}")
self.open_trades_count_label.config(text=str(len(self.mt5_client.open_trades))) #
Actualizar el contador de operaciones
def update_price_display(self, symbol, price):
"""Actualiza el precio del símbolo por defecto en la GUI."""
self.price_label.config(text=f"{price:,.5f}")
def update_symbols_monitored_display(self, count):
"""Actualiza el número de símbolos monitoreados en la GUI."""
self.symbols_monitored_label.config(text=str(count))
def add_trade_to_table(self, trade_data):
"""Añade una nueva operación a la tabla de historial."""
item_id = self.trade_table.insert("", [Link], values=trade_data)
self.trade_table.yview_moveto(1) # Desplazar a la última entrada
return item_id
def update_trade_result(self, gui_item_id, result_text, profit_loss_text):
"""Actualiza el resultado de una operación en la tabla de historial."""
current_values = list(self.trade_table.item(gui_item_id, 'values'))
if len(current_values) < 6:
current_values.extend([""] * (6 - len(current_values)))
if profit_loss_text:
new_values = current_values[:5] + [f"{result_text}
({profit_loss_text})"]
else:
new_values = current_values[:5] + [result_text]
self.trade_table.item(gui_item_id, values=new_values)
self.update_account_display(self.mt5_client.get_account_info()) # Forzar
actualización del balance/contador de operaciones
def on_trade_table_select(self, event):
"""Maneja la selección de una fila en la tabla de operaciones."""
selected_items = self.trade_table.selection()
if selected_items:
item_id = selected_items[0]
tags = self.trade_table.item(item_id, 'tags')
# Asegurarse de que tags no esté vacío y contenga un ID numérico
if tags and tags[0] and str(tags[0]).isdigit(): # [Link]() para
verificar que es un número
position_id = int(tags[0])
if position_id in self.mt5_client.open_trades:
self.close_selected_trade_button.config(state=[Link])
return # Salir temprano si se encuentra una operación abierta
válida
# Si no se selecciona ningún elemento, o el elemento seleccionado no es una
operación abierta de nuestro bot, deshabilitar el botón
self.close_selected_trade_button.config(state=[Link])
def close_selected_trade(self):
"""Cierra la operación seleccionada en la tabla."""
selected_items = self.trade_table.selection()
if not selected_items:
[Link]("Cerrar Operación", "Por favor, selecciona una
operación para cerrar.")
return
item_id = selected_items[0]
tags = self.trade_table.item(item_id, 'tags')
if not tags or not tags[0] or not str(tags[0]).isdigit():
[Link]("Error", "No se encontró el ID de posición válido
para la operación seleccionada.")
self.close_selected_trade_button.config(state=[Link])
return
try:
position_id = int(tags[0])
except ValueError: # Debería ser capturado por la verificación isdigit(),
pero por seguridad
[Link]("Error", "ID de posición no válido.")
self.close_selected_trade_button.config(state=[Link])
return
if position_id not in self.mt5_client.open_trades:
[Link]("Error", f"La operación (ID: {position_id}) ya no
está abierta o no fue iniciada por el bot.")
self.trade_table.selection_remove(item_id)
self.close_selected_trade_button.config(state=[Link])
return
if [Link]("Confirmar Cierre", f"¿Estás seguro que deseas
cerrar la operación con ID: {position_id}?"):
self.log_message(f"Solicitando cierre de operación ID: {position_id}
desde la GUI.")
[Link](target=self._execute_close_trade,
args=(position_id,)).start()
def _execute_close_trade(self, position_id):
"""Ejecuta el cierre de una operación en un hilo separado."""
[Link](0, self.close_selected_trade_button.config,
state=[Link])
success = self.mt5_client.close_position(position_id)
if success:
[Link](0, [Link], "Cierre Exitoso", f"La
operación {position_id} ha sido enviada para cerrar.")
[Link](0, self.trade_table.selection_remove,
self.trade_table.focus())
else:
[Link](0, [Link], "Error al Cerrar", f"No se
pudo cerrar la operación {position_id}.")
def show_trade_confirmation_popup(self, signal_data, gui_item_id):
"""Muestra una ventana emergente para confirmar operaciones manuales."""
popup = [Link]([Link])
[Link]("¡SEÑAL DETECTADA!")
[Link]("480x450") # Ajustado para más espacio para los nuevos
indicadores
popup.grab_set()
[Link](popup, text=f"¡NUEVA SEÑAL DETECTADA!", font=("Arial", 16,
"bold"), fg="blue").pack(pady=10)
[Link](popup, text=f"Par: {signal_data['symbol']}", font=("Arial",
12)).pack(anchor="w", padx=20)
[Link](popup, text=f"Dirección: {signal_data['direction']}",
font=("Arial", 12)).pack(anchor="w", padx=20)
[Link](popup, text=f"Estrategia: {signal_data['strategy']}",
font=("Arial", 12)).pack(anchor="w", padx=20)
[Link](popup, text=f"Entrada (Cierre Vela):
{signal_data['entry_price']:.5f}", font=("Arial", 12)).pack(anchor="w", padx=20)
# Mostrar valores de RSI y SMA si están disponibles
if signal_data.get('current_rsi') != "N/A":
[Link](popup, text=f"RSI: {signal_data['current_rsi']}",
font=("Arial", 10)).pack(anchor="w", padx=20)
if signal_data.get('sma_fast') != "N/A" and signal_data.get('sma_slow') !=
"N/A":
[Link](popup, text=f"SMA Rápida: {signal_data['sma_fast']}, SMA
Lenta: {signal_data['sma_slow']}", font=("Arial", 10)).pack(anchor="w", padx=20)
# Mostrar valores de Volumen y VMA si están disponibles
if signal_data.get('current_volume') != "N/A" and
signal_data.get('vma_volume') != "N/A":
[Link](popup, text=f"Volumen: {signal_data['current_volume']} (VMA:
{signal_data['vma_volume']})", font=("Arial", 10)).pack(anchor="w", padx=20)
# Mostrar valores de ADX, +DI y -DI si están disponibles
if signal_data.get('adx_value') != "N/A":
[Link](popup, text=f"ADX: {signal_data['adx_value']} (+DI:
{signal_data['plus_di']}, -DI: {signal_data['minus_di']})", font=("Arial",
10)).pack(anchor="w", padx=20)
strategy_params = self.get_current_strategy_params()
configured_volume = strategy_params['trade_volume'] if strategy_params else
"ERROR"
if strategy_params and strategy_params['use_dynamic_sl_tp']:
sl_info = f"SL Dinámico (Buffer:
{strategy_params['sl_tp_buffer_pips']})"
tp_info = f"TP Dinámico (R:R:{strategy_params['risk_reward_ratio']})"
elif strategy_params:
sl_info = f"SL Fijo: {strategy_params['sl_pips']} Pips"
tp_info = f"TP Fijo: {strategy_params['tp_pips']} Pips"
else:
sl_info = "ERROR"
tp_info = "ERROR"
[Link](popup, text=f"Volumen a operar: {configured_volume}",
font=("Arial", 12, "bold"), fg="purple").pack(anchor="w", padx=20, pady=5)
[Link](popup, text=f"{sl_info}", font=("Arial", 12, "bold"),
fg="purple").pack(anchor="w", padx=20, pady=2)
[Link](popup, text=f"{tp_info}", font=("Arial", 12, "bold"),
fg="purple").pack(anchor="w", padx=20, pady=2)
button_frame = [Link](popup)
button_frame.pack(pady=15)
def confirm_action():
self.mt5_client.signal_confirmed = True
self.mt5_client.signal_event.set()
[Link]()
def reject_action():
self.mt5_client.signal_confirmed = False
self.mt5_client.signal_event.set()
self.update_trade_result(gui_item_id, "RECHAZADA/EXPIRADA", "")
[Link]()
[Link](button_frame, text="✅ Confirmar Operación",
command=confirm_action, bg="green", fg="white", font=("Arial", 10,
"bold")).pack(side=[Link], padx=10)
[Link](button_frame, text="❌ Rechazar Operación", command=reject_action,
bg="red", fg="white", font=("Arial", 10, "bold")).pack(side=[Link], padx=10)
def on_popup_close():
reject_action()
[Link]()
[Link]("WM_DELETE_WINDOW", on_popup_close)
def toggle_auto_mode(self):
"""Alterna el modo de operación entre automático y manual."""
if self.auto_mode_var.get():
self.log_message("Modo de Operación: AUTOMÁTICO (Las operaciones se
ejecutarán sin confirmación).")
else:
self.log_message("Modo de Operación: MANUAL (Las operaciones requerirán
tu confirmación).")
# --- INICIO DE LA APLICACIÓN ---
if __name__ == "__main__":
root = [Link]()
app = TradingApp(root)
def on_closing():
"""Maneja el evento de cierre de la ventana principal."""
if [Link]("Salir", "¿Quieres salir de la aplicación y
desconectar MT5?"):
if not app.remember_credentials_var.get() and
[Link](CREDENTIALS_FILE):
try:
[Link](CREDENTIALS_FILE)
app.log_message("Archivo de credenciales eliminado al salir.")
except Exception as e:
app.log_message(f"Error al eliminar archivo de credenciales al
salir: {e}")
app.disconnect_mt5()
[Link]()
[Link]("WM_DELETE_WINDOW", on_closing)
[Link]()