TRY-1/mt5_bot.py
2025-10-03 14:28:05 +00:00

706 lines
No EOL
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()