706 lines
		
	
	
	
		
			37 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
		
		
			
		
	
	
			706 lines
		
	
	
	
		
			37 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
								 | 
							
								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']<c3['Open']: ob_candle=c3
							 | 
						||
| 
								 | 
							
								            elif c2['Close']<c2['Open'] and c3['Close']<c3['Open'] and c2['is_swing_high']: ob_candle=c3
							 | 
						||
| 
								 | 
							
								            if ob_candle is not None and v_candle['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']<c2['Open'] and c3['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']<ob_candle['Open']:
							 | 
						||
| 
								 | 
							
								                zones.append({'type': 'Bearish OB', 'time': v_candle.name, 'low': ob_candle['Low'], 'high': ob_candle['High']})
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								        for i in range(2, len(h1_df_recent) - 1):
							 | 
						||
| 
								 | 
							
								            c1, _, c3 = h1_df_recent.iloc[i-2], h1_df_recent.iloc[i-1], h1_df_recent.iloc[i]
							 | 
						||
| 
								 | 
							
								            if c3['Low']>c1['High']:
							 | 
						||
| 
								 | 
							
								                zones.append({'type': 'Bullish FVG', 'time': c3.name, 'low': c1['High'], 'high': c3['Low']})
							 | 
						||
| 
								 | 
							
								            if c3['High']<c1['Low']:
							 | 
						||
| 
								 | 
							
								                zones.append({'type': 'Bearish FVG', 'time': c3.name, 'low': c3['High'], 'high': c1['Low']})
							 | 
						||
| 
								 | 
							
								        return zones
							 | 
						||
| 
								 | 
							
								
							 | 
						||
| 
								 | 
							
								    def _find_breaker_setup(self, df_slice):
							 | 
						||
| 
								 | 
							
								        df_with_swings = self._find_swing_points(df_slice)
							 | 
						||
| 
								 | 
							
								        swing_highs = df_with_swings[df_with_swings['is_swing_high']]
							 | 
						||
| 
								 | 
							
								        swing_lows = df_with_swings[df_with_swings['is_swing_low']]
							 | 
						||
| 
								 | 
							
								        
							 | 
						||
| 
								 | 
							
								        for i in range(len(swing_highs)): # Bearish
							 | 
						||
| 
								 | 
							
								            pointA = swing_highs.iloc[i]
							 | 
						||
| 
								 | 
							
								            potential_Bs = swing_lows[swing_lows.index > 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()
							 |