0% encontró este documento útil (0 votos)
11 vistas40 páginas

Configuración de Exness MT5 para Trading

El documento describe la configuración y funciones de un bot de trading en MetaTrader 5, incluyendo parámetros para estrategias de trading como Donchian, RSI, SMA, volumen y ADX. Se establecen configuraciones predeterminadas para el inicio de sesión, símbolos a monitorear y límites de operaciones simultáneas. Además, se implementan funciones para conectarse a MT5, obtener información de la cuenta y calcular indicadores técnicos necesarios para la toma de decisiones de trading.

Cargado por

vicsan0403
Derechos de autor
© All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como TXT, PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
11 vistas40 páginas

Configuración de Exness MT5 para Trading

El documento describe la configuración y funciones de un bot de trading en MetaTrader 5, incluyendo parámetros para estrategias de trading como Donchian, RSI, SMA, volumen y ADX. Se establecen configuraciones predeterminadas para el inicio de sesión, símbolos a monitorear y límites de operaciones simultáneas. Además, se implementan funciones para conectarse a MT5, obtener información de la cuenta y calcular indicadores técnicos necesarios para la toma de decisiones de trading.

Cargado por

vicsan0403
Derechos de autor
© All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como TXT, PDF, TXT o lee en línea desde Scribd

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]()

También podría gustarte