import MetaTrader5 as mt5 import pandas as pd import pytz import time as sleep_timer from datetime import datetime, timedelta, time import logging import json import requests import sqlite3 import threading # ============================================================================== # == CONFIGURATION & LOGGING # ============================================================================== def load_config(filename='config.json'): try: with open(filename, 'r') as f: return json.load(f) except FileNotFoundError: logging.critical(f"CRITICAL: Config file '{filename}' not found.") quit() except json.JSONDecodeError: logging.critical(f"CRITICAL: Config file '{filename}' is not valid JSON.") quit() def setup_logging(): if logging.getLogger().hasHandlers(): return class ESTFormatter(logging.Formatter): def formatTime(self, record, datefmt=None): dt = datetime.fromtimestamp(record.created, tz=pytz.timezone('America/New_York')) if datefmt: return dt.strftime(datefmt) return dt.isoformat() logger = logging.getLogger() logger.setLevel(logging.INFO) formatter = ESTFormatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S') console_handler = logging.StreamHandler() console_handler.setFormatter(formatter) file_handler = logging.FileHandler('trading_bot.log', encoding='utf-8') file_handler.setFormatter(formatter) logger.addHandler(console_handler) logger.addHandler(file_handler) # ============================================================================== # == STATE MANAGER CLASS (SQLite VERSION) # ============================================================================== class StateManager: def __init__(self, db_file='bot_state.db'): self.db_file = db_file self.conn = sqlite3.connect(self.db_file, detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES, check_same_thread=False) self._create_tables() def _create_tables(self): cursor = self.conn.cursor() cursor.execute("CREATE TABLE IF NOT EXISTS traded_setups (id INTEGER PRIMARY KEY AUTOINCREMENT, setup_id TEXT UNIQUE NOT NULL)") cursor.execute("CREATE TABLE IF NOT EXISTS pending_orders (ticket INTEGER PRIMARY KEY, symbol TEXT NOT NULL, expiry_time TEXT NOT NULL)") cursor.execute(''' CREATE TABLE IF NOT EXISTS trades ( ticket INTEGER PRIMARY KEY, symbol TEXT NOT NULL, type TEXT NOT NULL, volume REAL NOT NULL, open_time TIMESTAMP NOT NULL, open_price REAL NOT NULL, sl REAL NOT NULL, tp REAL NOT NULL, close_time TIMESTAMP, close_price REAL, profit REAL, partials_taken INTEGER DEFAULT 0 )''') cursor.execute("CREATE TABLE IF NOT EXISTS bot_settings (key TEXT PRIMARY KEY, value TEXT)") self.conn.commit() def get_setting(self, key): cursor = self.conn.cursor() cursor.execute("SELECT value FROM bot_settings WHERE key = ?", (key,)) row = cursor.fetchone() return row[0] if row else None def set_setting(self, key, value): cursor = self.conn.cursor() cursor.execute("INSERT OR REPLACE INTO bot_settings (key, value) VALUES (?, ?)", (key, value)) self.conn.commit() def get_performance_report(self): try: df = pd.read_sql_query("SELECT * FROM trades WHERE close_time IS NOT NULL", self.conn) if df.empty: return None total_trades, wins, losses = len(df), len(df[df['profit'] > 0.01]), len(df[df['profit'] < -0.01]) breakevens = total_trades - wins - losses win_rate = (wins / (wins + losses) * 100) if (wins + losses) > 0 else 0 total_profit, gross_profit, gross_loss = df['profit'].sum(), df[df['profit'] > 0]['profit'].sum(), abs(df[df['profit'] < 0]['profit'].sum()) profit_factor = gross_profit / gross_loss if gross_loss > 0 else float('inf') return {"total_trades": total_trades, "wins": wins, "losses": losses, "breakevens": breakevens, "win_rate": f"{win_rate:.2f}%", "net_profit": f"${total_profit:,.2f}", "profit_factor": f"{profit_factor:.2f}"} except Exception as e: logging.error(f"Error generating performance report: {e}") return None def get_open_trade_tickets(self): cursor = self.conn.cursor() cursor.execute("SELECT ticket FROM trades WHERE close_time IS NULL") return [row[0] for row in cursor.fetchall()] def log_new_trade(self, pos): cursor = self.conn.cursor() trade_type = "BUY" if pos.type == mt5.ORDER_TYPE_BUY else "SELL" cursor.execute("INSERT OR IGNORE INTO trades (ticket, symbol, type, volume, open_time, open_price, sl, tp) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", (pos.ticket, pos.symbol, trade_type, pos.volume, datetime.fromtimestamp(pos.time, tz=pytz.utc), pos.price_open, pos.sl, pos.tp)) self.conn.commit() def update_trade_partials_taken(self, ticket): cursor = self.conn.cursor() cursor.execute("UPDATE trades SET partials_taken = 1 WHERE ticket = ?", (ticket,)) self.conn.commit() def update_closed_trade(self, ticket, close_time, close_price, profit): cursor = self.conn.cursor() cursor.execute("UPDATE trades SET close_time = ?, close_price = ?, profit = ? WHERE ticket = ?", (close_time, close_price, profit, ticket)) self.conn.commit() def get_trade_info(self, ticket): cursor = self.conn.cursor() cursor.execute("SELECT partials_taken FROM trades WHERE ticket = ?", (ticket,)) row = cursor.fetchone() if row: return {'partials_taken': row[0]} return None def get_traded_setups(self): cursor = self.conn.cursor() cursor.execute("SELECT setup_id FROM traded_setups") return [row[0] for row in cursor.fetchall()] def add_traded_setup(self, setup_id): cursor = self.conn.cursor() cursor.execute("INSERT OR IGNORE INTO traded_setups (setup_id) VALUES (?)", (setup_id,)) cursor.execute("DELETE FROM traded_setups WHERE id NOT IN (SELECT id FROM traded_setups ORDER BY id DESC LIMIT 10)") self.conn.commit() def get_pending_orders(self): cursor = self.conn.cursor() cursor.execute("SELECT ticket, symbol, expiry_time FROM pending_orders") orders = {} ny_tz = pytz.timezone('America/New_York') for row in cursor.fetchall(): try: expiry_dt_naive = datetime.fromisoformat(row[2]) orders[str(row[0])] = {"symbol": row[1], "expiry_time": ny_tz.localize(expiry_dt_naive)} except (ValueError, TypeError): logging.error(f"Could not parse expiry time '{row[2]}' for ticket {row[0]}") return orders def add_pending_order(self, ticket, symbol, expiry_time): cursor = self.conn.cursor() cursor.execute("INSERT OR IGNORE INTO pending_orders (ticket, symbol, expiry_time) VALUES (?, ?, ?)", (ticket, symbol, expiry_time.strftime('%Y-%m-%dT%H:%M:%S'))) self.conn.commit() def remove_pending_order(self, ticket): cursor = self.conn.cursor() cursor.execute("DELETE FROM pending_orders WHERE ticket = ?", (int(ticket),)) self.conn.commit() def close_connection(self): self.conn.close() logging.info("\nDatabase connection closed.") # ============================================================================== # == STRATEGY CLASS # ============================================================================== class BreakerStrategy: def __init__(self, config): self.config = config def _find_swing_points(self, df): df = df.copy() df['is_swing_high'] = (df['High'] > df['High'].shift(1)) & (df['High'] > df['High'].shift(-1)) & (df['High'] > df['High'].shift(-2)) df['is_swing_low'] = (df['Low'] < df['Low'].shift(1)) & (df['Low'] < df['Low'].shift(-1)) & (df['Low'] < df['Low'].shift(-2)) return df def find_htf_zones(self, h1_df): h1_df_recent = h1_df.tail(20) zones = [] df_swing = self._find_swing_points(h1_df_recent.copy()) for i in range(4, len(df_swing) - 1): c1, c2, c3, v_candle = df_swing.iloc[i-4], df_swing.iloc[i-3], df_swing.iloc[i-2], df_swing.iloc[i-1] ob_candle = None if c2['Close']>c2['Open'] and c3['Close']ob_candle['Open']: zones.append({'type': 'Bullish OB', 'time': v_candle.name, 'low': ob_candle['Low'], 'high': ob_candle['High']}) ob_candle=None if c2['Close']c3['Open']: ob_candle=c3 elif c2['Close']>c2['Open'] and c3['Close']>c3['Open'] and c2['is_swing_low']: ob_candle=c3 if ob_candle is not None and v_candle['Close']c1['High']: zones.append({'type': 'Bullish FVG', 'time': c3.name, 'low': c1['High'], 'high': c3['Low']}) if c3['High'] pointA.name] if not potential_Bs.empty: pointB = potential_Bs.iloc[0] breaker_range = df_with_swings.loc[pointA.name:pointB.name] down_candles = breaker_range[breaker_range['Close'] < breaker_range['Open']] if down_candles.empty: continue breaker_candle = down_candles.loc[down_candles['Low'].idxmin()] sweep_candidates = df_slice[(df_slice.index > pointB.name) & (df_slice['High'] > pointA.High)] if not sweep_candidates.empty: pointC = sweep_candidates.iloc[0] if not df_with_swings.loc[pointB.name:pointC.name].iloc[1:-1]['is_swing_high'].any(): confirmation_window = df_slice[df_slice.index > pointC.name].iloc[:2] if not confirmation_window.empty and confirmation_window.iloc[0]['Close'] < breaker_candle.Low: pointD = confirmation_window.iloc[0] if not breaker_candle.is_swing_high: return {'type': 'Bearish', 'breaker_candle': breaker_candle, 'pointA': pointA, 'pointB': pointB, 'pointC': pointC, 'pointD': pointD} for i in range(len(swing_lows)): # Bullish pointA = swing_lows.iloc[i] potential_Bs = swing_highs[swing_highs.index > pointA.name] if not potential_Bs.empty: pointB = potential_Bs.iloc[0] breaker_range = df_with_swings.loc[pointA.name:pointB.name] up_candles = breaker_range[breaker_range['Close'] > breaker_range['Open']] if up_candles.empty: continue breaker_candle = up_candles.loc[up_candles['High'].idxmax()] sweep_candidates = df_slice[(df_slice.index > pointB.name) & (df_slice['Low'] < pointA.Low)] if not sweep_candidates.empty: pointC = sweep_candidates.iloc[0] if not df_with_swings.loc[pointB.name:pointC.name].iloc[1:-1]['is_swing_low'].any(): confirmation_window = df_slice[df_slice.index > pointC.name].iloc[:2] if not confirmation_window.empty and confirmation_window.iloc[0]['Close'] > breaker_candle.High: pointD = confirmation_window.iloc[0] if not breaker_candle.is_swing_low: return {'type': 'Bullish', 'breaker_candle': breaker_candle, 'pointA': pointA, 'pointB': pointB, 'pointC': pointC, 'pointD': pointD} return None def find_setups(self, all_tf_data): search_end_time = all_tf_data[1].index[-1] search_start_time = search_end_time - timedelta(minutes=120) for tf in [5, 1]: df = all_tf_data.get(tf) if df is None: continue analysis_slice = df.loc[search_start_time:search_end_time] if analysis_slice.empty or len(analysis_slice) < 5: continue breaker_setup = self._find_breaker_setup(analysis_slice) if breaker_setup: pointD_time = breaker_setup['pointD'].name if pointD_time >= df.index[-3]: return breaker_setup, tf return None, None # ============================================================================== # == MT5 HANDLER CLASS # ============================================================================== class MT5Handler: def __init__(self, config): self.config = config self.magic_number = self.config.get('magic_number') def connect(self): if not mt5.initialize(): logging.critical(f"initialize() failed, error code = {mt5.last_error()}") return False logging.info("āœ… MT5 Bot Connected!") return True def disconnect(self): mt5.shutdown() logging.info("\nšŸ”Œ Bot stopped and disconnected.") def check_connection(self): if not mt5.terminal_info(): logging.warning("\nMT5 connection lost. Attempting to reconnect...") mt5.shutdown() sleep_timer.sleep(10) if self.connect(): logging.info("\nSuccessfully reconnected to MT5.") return True else: logging.error("\nFailed to reconnect to MT5. Will retry next cycle.") return False return True def get_all_timeframe_data(self, symbol): all_tf_data = {} timeframe_map = {1: mt5.TIMEFRAME_M1, 2: mt5.TIMEFRAME_M2, 3: mt5.TIMEFRAME_M3, 5: mt5.TIMEFRAME_M5, 60: mt5.TIMEFRAME_H1} for tf_int, tf_mt5 in timeframe_map.items(): num_candles = 500 if tf_int == 60 else 300 df = self._get_candles(symbol, tf_mt5, num_candles) if df is None or df.empty: logging.warning(f"Could not fetch M{tf_int} data for {symbol}.") return None all_tf_data[tf_int] = df return all_tf_data def _get_candles(self, symbol, timeframe, num_candles): rates = mt5.copy_rates_from_pos(symbol, timeframe, 0, num_candles) if rates is None: return None df = pd.DataFrame(rates) df['time'] = pd.to_datetime(df['time'], unit='s') df.set_index('time', inplace=True) df.index = df.index.tz_localize('Europe/Athens').tz_convert('America/New_York') df.rename(columns={'open': 'Open', 'high': 'High', 'low': 'Low', 'close': 'Close'}, inplace=True) return df def place_pending_order(self, symbol, order_type, volume, entry_price, sl_price, tp_price): symbol_info = mt5.symbol_info(symbol) if symbol_info is None: logging.error(f"Failed to get symbol info for {symbol}.") return None point = symbol_info.point stops_level = symbol_info.trade_stops_level min_distance = stops_level * point if abs(entry_price - sl_price) < min_distance or abs(entry_price - tp_price) < min_distance: logging.warning(f"\nSL/TP for {symbol} is too close to entry price (Broker minimum: {min_distance}). Trade skipped.") return None request = { "action": mt5.TRADE_ACTION_PENDING, "symbol": symbol, "volume": volume, "type": order_type, "price": entry_price, "sl": sl_price, "tp": tp_price, "magic": self.magic_number, "comment": "BreakerBot v3.2", "type_time": mt5.ORDER_TIME_GTC, "type_filling": mt5.ORDER_FILLING_FOK, } result = mt5.order_send(request) if result.retcode != mt5.TRADE_RETCODE_DONE: logging.warning(f"\nPending order failed for {symbol}: {result.comment}") else: logging.info(f"\nāœ… Pending order placed for {symbol}! Order #{result.order}") return result def place_partial_close_order(self, position, volume_to_close): if position.type == mt5.ORDER_TYPE_BUY: order_type, price = mt5.ORDER_TYPE_SELL, mt5.symbol_info_tick(position.symbol).bid else: order_type, price = mt5.ORDER_TYPE_BUY, mt5.symbol_info_tick(position.symbol).ask request = { "action": mt5.TRADE_ACTION_DEAL, "position": position.ticket, "symbol": position.symbol, "volume": volume_to_close, "type": order_type, "price": price, "magic": self.magic_number, "comment": "Partial Take Profit" } result = mt5.order_send(request) if result and result.retcode == mt5.TRADE_RETCODE_DONE: logging.info(f"\nāœ… Successfully closed {volume_to_close} lots of position #{position.ticket}.") return True else: logging.error(f"\nāŒ Failed to close partial volume for position #{position.ticket}: {result.comment if result else 'No result'}") return False def modify_position_sl(self, ticket, new_sl): request = {"action": mt5.TRADE_ACTION_SLTP, "position": ticket, "sl": new_sl} result = mt5.order_send(request) if result and result.retcode == mt5.TRADE_RETCODE_DONE: logging.info(f"\nāœ… Successfully moved SL for position #{ticket}.") return True else: logging.error(f"\nāŒ Failed to modify SL for position #{ticket}: {result.comment if result else 'No result'}") return False def manage_open_trades(self, state_manager): positions = mt5.positions_get(magic=self.magic_number) if not positions: return for pos in positions: trade_info = state_manager.get_trade_info(pos.ticket) if trade_info and trade_info['partials_taken'] == 1: continue initial_risk = abs(pos.price_open - pos.sl) if initial_risk == 0: continue if pos.type == mt5.ORDER_TYPE_BUY: breakeven_trigger_price = pos.price_open + (initial_risk * 2) if mt5.symbol_info_tick(pos.symbol).bid >= breakeven_trigger_price: self.handle_partial_tp_and_be(pos, state_manager) elif pos.type == mt5.ORDER_TYPE_SELL: breakeven_trigger_price = pos.price_open - (initial_risk * 2) if mt5.symbol_info_tick(pos.symbol).ask <= breakeven_trigger_price: self.handle_partial_tp_and_be(pos, state_manager) def handle_partial_tp_and_be(self, position, state_manager): logging.info(f"\nPosition #{position.ticket} reached 2R. Taking partials and moving to BE.") symbol_info = mt5.symbol_info(position.symbol) partial_volume = round(position.volume * 0.3, 2) if partial_volume < symbol_info.volume_min: partial_volume = symbol_info.volume_min partial_volume = round(partial_volume / symbol_info.volume_step) * symbol_info.volume_step if partial_volume >= position.volume: logging.warning("\nPartial volume is >= position volume. Moving to BE only.") if self.modify_position_sl(position.ticket, position.price_open): state_manager.update_trade_partials_taken(position.ticket) message = f"šŸ“ˆ *Stop Loss Moved to Breakeven*\nSymbol: `{position.symbol}`\nPosition: `#{position.ticket}`" send_telegram_alert(message, self.config) else: if self.place_partial_close_order(position, partial_volume): self.modify_position_sl(position.ticket, position.price_open) state_manager.update_trade_partials_taken(position.ticket) message = f"šŸ’° *Partial Profit Taken & BE*\nSymbol: `{position.symbol}`\nClosed: `{partial_volume}` lots\nPosition: `#{position.ticket}`" send_telegram_alert(message, self.config) def manage_pending_orders(self, state_manager): pending_orders = state_manager.get_pending_orders() if not pending_orders: return now_ny = datetime.now(pytz.timezone('America/New_York')) for ticket, order_data in list(pending_orders.items()): if now_ny > order_data['expiry_time']: logging.info(f"\nPending order #{ticket} for {order_data['symbol']} has expired. Attempting to cancel.") request = {"action": mt5.TRADE_ACTION_REMOVE, "order": int(ticket)} result = mt5.order_send(request) if result.retcode == mt5.TRADE_RETCODE_DONE: logging.info(f"\nāœ… Successfully cancelled expired order #{ticket}.") state_manager.remove_pending_order(ticket) else: if not mt5.orders_get(ticket=int(ticket)): logging.info(f"\nOrder #{ticket} no longer exists. Removing from tracking.") state_manager.remove_pending_order(ticket) else: logging.error(f"\nFailed to cancel expired order #{ticket}. Reason: {result.comment}") # ============================================================================== # == HELPER FUNCTIONS # ============================================================================== def send_telegram_alert(message, config): bot_token = config.get('telegram_bot_token') chat_id = config.get('telegram_chat_id') if not bot_token or not chat_id or 'YOUR_BOT_TOKEN' in bot_token: return url = f"https://api.telegram.org/bot{bot_token}/sendMessage" payload = {'chat_id': chat_id, 'text': message, 'parse_mode': 'Markdown'} try: response = requests.post(url, json=payload, timeout=10) if response.status_code != 200: logging.error(f"Failed to send Telegram alert: {response.text}") except Exception as e: logging.error(f"Exception while sending Telegram alert: {e}") def format_breaker_details(breaker_setup): bc, pointA, pointB, pointC, pointD = breaker_setup['breaker_candle'], breaker_setup['pointA'], breaker_setup['pointB'], breaker_setup['pointC'], breaker_setup['pointD'] a_marker, b_marker = ("-", "+") if breaker_setup['type'] == 'Bullish' else ("+", "-") c_marker = "+" if pointC['Close'] > pointC['Open'] else "-" details = ( f"*Breaker Time:* `{bc.name.strftime('%H:%M:%S')}`\n" f"*Breaker Candle:* `[O:{bc['Open']:.5f} H:{bc['High']:.5f} L:{bc['Low']:.5f} C:{bc['Close']:.5f}]`\n" f"*Point A{a_marker}:* `{pointA.name.strftime('%H:%M:%S')}`\n" f"*Point B{b_marker}:* `{pointB.name.strftime('%H:%M:%S')}`\n" f"*Point C (Sweep){c_marker}:* `{pointC.name.strftime('%H:%M:%S')}`\n" f"*Point D (Confirmation):* `{pointD.name.strftime('%H:%M:%S')}`\n" f"*Confirmation Candle:* `[O:{pointD['Open']:.5f} H:{pointD['High']:.5f} L:{pointD['Low']:.5f} C:{pointD['Close']:.5f}]`" ) return details def calculate_dynamic_lot_size(symbol, risk_percent, sl_price, entry_price): try: account_info = mt5.account_info() if account_info is None: return None balance = account_info.balance risk_amount_account_currency = balance * risk_percent sl_distance_price = abs(entry_price - sl_price) if sl_distance_price == 0: return None symbol_info = mt5.symbol_info(symbol) if symbol_info is None: return None quote_currency = symbol_info.currency_profit account_currency = account_info.currency conversion_rate = 1.0 if quote_currency != account_currency: pair_forward, pair_backward = f"{quote_currency}{account_currency}", f"{account_currency}{quote_currency}" tick_forward = mt5.symbol_info_tick(pair_forward) if tick_forward and tick_forward.ask != 0: conversion_rate = tick_forward.ask else: tick_backward = mt5.symbol_info_tick(pair_backward) if tick_backward and tick_backward.bid != 0: conversion_rate = 1 / tick_backward.bid else: logging.warning(f"\nCannot find conversion rate for {quote_currency} to {account_currency}.") return None loss_in_quote_currency_per_lot = sl_distance_price * symbol_info.trade_contract_size loss_in_account_currency_per_lot = loss_in_quote_currency_per_lot * conversion_rate if loss_in_account_currency_per_lot == 0: return None lot_size = risk_amount_account_currency / loss_in_account_currency_per_lot volume_step, min_volume, max_volume = symbol_info.volume_step, symbol_info.volume_min, symbol_info.volume_max lot_size = max(min_volume, min(max_volume, lot_size)) lot_size = round(lot_size / volume_step) * volume_step return round(lot_size, 2) except Exception as e: logging.error(f"\nError in lot size calculation: {e}", exc_info=True) return None def is_in_trading_session(config): ny_tz = pytz.timezone('America/New_York') now_ny = datetime.now(ny_tz) current_time = now_ny.time() if now_ny.weekday() >= 5: return False sessions = config.get('trading_sessions', []) for session in sessions: try: start = time.fromisoformat(session['start']) end = time.fromisoformat(session['end']) if start <= current_time <= end: return True except (ValueError, KeyError): logging.warning(f"\nInvalid session format in config.json: {session}") continue return False def check_and_log_new_trades(state_manager, magic_number): open_positions = mt5.positions_get(magic=magic_number) if not open_positions: return logged_open_tickets = state_manager.get_open_trade_tickets() for pos in open_positions: if pos.ticket not in logged_open_tickets: logging.info(f"\nNew trade detected: Position #{pos.ticket}. Logging to database.") state_manager.log_new_trade(pos) def check_for_closed_trades(state_manager, config): logged_open_tickets = state_manager.get_open_trade_tickets() if not logged_open_tickets: return magic_number = config.get('magic_number') open_positions = mt5.positions_get(magic=magic_number) open_position_tickets = {pos.ticket for pos in open_positions} if open_positions else set() for ticket in logged_open_tickets: if ticket not in open_position_tickets: logging.info(f"\nClosed trade detected: Position #{ticket}. Fetching history...") from_date = datetime.now(pytz.utc) - timedelta(days=3) deals = mt5.history_deals_get(from_date, datetime.now(pytz.utc)) if deals is None: continue profit = 0; close_time = None; close_price = None; symbol = "" for deal in deals: if deal.position_id == ticket: symbol = deal.symbol if deal.entry == 1: profit += deal.profit close_time = datetime.fromtimestamp(deal.time, tz=pytz.utc) close_price = deal.price if close_time: state_manager.update_closed_trade(ticket, close_time, close_price, profit) outcome = "Win" if profit > 0.01 else "Loss" if profit < -0.01 else "Breakeven" message = (f"{'šŸ†' if outcome == 'Win' else 'āŒ'} *Trade Closed ({outcome})*\n" f"Symbol: `{symbol}`\n" f"Ticket: `#{ticket}`\n" f"Profit: `${profit:.2f}`") send_telegram_alert(message, config) # ============================================================================== # == MAIN BOT LOGIC # ============================================================================== def run_bot(config, state_manager): logging.info("šŸš€ Breaker Bot v3.2 is running...") mt5_handler = MT5Handler(config) strategy = BreakerStrategy(config) symbols_to_trade = config['symbols_to_trade'] risk_percent = config['risk_percent'] fixed_rr = config['fixed_rr'] active_htf_zones = {symbol: [] for symbol in symbols_to_trade} last_h1_scan_times = {} loop_count = 0 spinner_chars = ['|', '/', '-', '\\'] while True: try: now_ny = datetime.now(pytz.timezone('America/New_York')) if not mt5_handler.check_connection(): sleep_timer.sleep(30) continue check_and_log_new_trades(state_manager, config.get('magic_number')) check_for_closed_trades(state_manager, config) mt5_handler.manage_pending_orders(state_manager) mt5_handler.manage_open_trades(state_manager) if not is_in_trading_session(config): status_message = f"[{now_ny.strftime('%H:%M:%S')} EST] Outside of trading sessions. Pausing... {spinner_chars[loop_count % len(spinner_chars)]}" print(status_message, end='\r') loop_count += 1 sleep_timer.sleep(900) continue for symbol in symbols_to_trade: all_tf_data = mt5_handler.get_all_timeframe_data(symbol) if not all_tf_data: continue h1_df = all_tf_data.get(60) last_scan_time = last_h1_scan_times.get(symbol) if h1_df is not None and (last_scan_time is None or now_ny.hour != last_scan_time.hour): print() logging.info(f"Scanning for new H1 zones on {symbol}...") active_htf_zones[symbol] = strategy.find_htf_zones(h1_df) last_h1_scan_times[symbol] = now_ny h1_filter_passed = False aligned_zone = None if not active_htf_zones.get(symbol): h1_filter_passed = True else: fifteen_mins_ago = now_ny - timedelta(minutes=15) last_15m_data = all_tf_data[1].loc[fifteen_mins_ago:] if not last_15m_data.empty: for zone in active_htf_zones[symbol]: if (last_15m_data['Low'].min() <= zone['high'] and last_15m_data['High'].max() >= zone['low']): h1_filter_passed = True aligned_zone = zone break if not h1_filter_passed: continue breaker_setup, tf_found = strategy.find_setups(all_tf_data) if breaker_setup: if aligned_zone and \ (('Bullish' in breaker_setup['type'] and 'Bearish' in aligned_zone['type']) or \ ('Bearish' in breaker_setup['type'] and 'Bullish' in aligned_zone['type'])): continue setup_id_str = str((symbol, str(breaker_setup['pointD'].name))) if setup_id_str in state_manager.get_traded_setups(): continue print() details_log = format_breaker_details(breaker_setup) bc, C = breaker_setup['breaker_candle'], breaker_setup['pointC'] if breaker_setup['type'] == 'Bullish': pending_entry_price, order_type = bc['Low'], mt5.ORDER_TYPE_BUY_LIMIT else: pending_entry_price, order_type = bc['High'], mt5.ORDER_TYPE_SELL_LIMIT tick = mt5.symbol_info_tick(symbol) if tick is None: continue if order_type == mt5.ORDER_TYPE_BUY_LIMIT and pending_entry_price >= tick.ask: continue if order_type == mt5.ORDER_TYPE_SELL_LIMIT and pending_entry_price <= tick.bid: continue strategy_sl_price = C['High'] if breaker_setup['type'] == 'Bearish' else C['Low'] volume = calculate_dynamic_lot_size(symbol, risk_percent, strategy_sl_price, pending_entry_price) if volume is None or volume == 0: continue symbol_info = mt5.symbol_info(symbol) spread_in_price = symbol_info.ask - symbol_info.bid sl_tp_buffer = spread_in_price * 1.5 if breaker_setup['type'] == 'Bearish': final_sl_price = strategy_sl_price + sl_tp_buffer risk_distance = final_sl_price - pending_entry_price final_tp_price = pending_entry_price - (risk_distance * fixed_rr) else: final_sl_price = strategy_sl_price - sl_tp_buffer risk_distance = pending_entry_price - final_sl_price final_tp_price = pending_entry_price + (risk_distance * fixed_rr) digits = symbol_info.digits final_sl_price, final_tp_price = round(final_sl_price, digits), round(final_tp_price, digits) price_details = (f"*Pending Entry:* `{pending_entry_price:.{digits}f}`\n" f"*Stop Loss:* `{final_sl_price:.{digits}f}`\n" f"*Take Profit:* `{final_tp_price:.{digits}f}`\n" f"*Volume:* `{volume:.2f}`") message = f"*āœ… VALID {breaker_setup['type']} Setup Found on {symbol} (M{tf_found})*\n{details_log}\n\n{price_details}" logging.info(message.replace('*', '').replace('`', '')) send_telegram_alert(message, config) result = mt5_handler.place_pending_order(symbol, order_type, volume, pending_entry_price, final_sl_price, final_tp_price) if result and result.retcode == mt5.TRADE_RETCODE_DONE: state_manager.add_traded_setup(setup_id_str) order_ticket = result.order expiry_time = datetime.now(pytz.timezone('America/New_York')) + timedelta(minutes=(tf_found * 3)) state_manager.add_pending_order(order_ticket, symbol, expiry_time) logging.info(f"Order #{order_ticket} will expire at {expiry_time.strftime('%H:%M:%S')}") status_message = f"[{now_ny.strftime('%H:%M:%S')} EST] Bot is running... Last check complete. {spinner_chars[loop_count % len(spinner_chars)]}" print(status_message, end='\r') loop_count += 1 sleep_timer.sleep(60) except Exception as e: logging.error("\n A critical error occurred in the main loop:", exc_info=True) sleep_timer.sleep(60) # ============================================================================== # == SCRIPT EXECUTION # ============================================================================== if __name__ == "__main__": setup_logging() config = load_config() state_manager = StateManager() mt5_handler = MT5Handler(config) if not mt5_handler.connect(): quit() try: run_bot(config, state_manager) except KeyboardInterrupt: print("\nBot stopped by user.") finally: state_manager.close_connection() mt5_handler.disconnect()