Build an Automated Trading Bot

Connect Python to MetaTrader 5 and trade EURUSD on a demo account.

Introduction

30 Second Summary

Financial markets move around the clock, and professional traders don't sit at their screens waiting for the right moment to buy or sell. They use automated systems that watch for opportunities and execute trades in milliseconds.

In this project, you will build an automated trading bot in Python that connects to MetaTrader 5, retrieves live market data, and executes trades on a demo account using a moving average crossover strategy.

What You'll Build

A complete Python trading bot that runs locally on your Windows machine, connects to MetaTrader 5 via its Python API, and trades EURUSD automatically based on calculated moving average signals.

By the end of this project, you'll have:

  • A working connection to MetaTrader 5 with live market data retrieval and trading signal calculation using moving averages.
  • Automated trade execution with stop-loss, take-profit, and position sizing for risk management.
  • A production-style Python application with structured logging, graceful shutdown, and modular architecture.
  • Secret Mission: Add automatic reconnection logic with exponential backoff so the bot survives disconnections without manual intervention.

Are there any prerequisites?

This project requires a Windows machine (MetaTrader 5 only runs on Windows). You should have intermediate Python knowledge, VS Code installed, and Python installed on your system.

No real money is involved. You will use a free demo account with virtual funds throughout the entire project.

Before We Start

Before We Start...

Install MetaTrader 5 and Set Up Your Environment

Before your bot can trade, it needs a bridge between Python and the financial markets. MetaTrader 5 is a trading platform that provides that bridge through its Python API.

In this step, you will install MetaTrader 5, create a free demo account with virtual money, and set up a Python environment with all the packages the bot needs.

In this step, get ready to:
  • Install MetaTrader 5 and create a demo trading account.
  • Create a Python virtual environment and install required packages.
  • Verify that Python can communicate with MetaTrader 5.
Download and install MetaTrader 5

MetaTrader 5 is a desktop trading terminal that your Python scripts will control via interprocess communication. Both the terminal and your script must run on the same Windows machine.

  • Open your browser and navigate to the MetaTrader 5 download page.
  • Click the Download button to download mt5setup.exe.
  • Double-click mt5setup.exe to start the installer.
  • Review the license agreement, then click Next to begin downloading components.
  • When the installation finishes, click Finish. MetaTrader 5 will open automatically.

What if MetaTrader 5 does not open?

If the platform does not launch after clicking Finish, press the Windows key, type MetaTrader 5, and press Enter to open it manually.

Once MetaTrader 5 is open, create a demo account so the bot has a trading environment to work with.

  • In the MetaTrader 5 menu bar, click File.
  • Click Open an Account.
  • Select MetaQuotes-Demo from the server list and click Next.
  • Choose Open a demo account and click Next.
  • Fill in the registration details (name, email, account type) and click Next.
  • Record your login number, password, and server name somewhere safe. You will need these values later.

Why a demo account?

A demo account gives you a fully functional trading environment with virtual money. Your bot will execute real market orders against live price data, but no actual funds are at risk.

Don't see MetaQuotes-Demo in the server list?

Make sure your computer is connected to the internet. MetaTrader 5 downloads the server list on first launch. If the list is empty, close the dialog, wait a few seconds, then try File > Open an Account again.

Help me connect to a demo server in MetaTrader 5

Create a virtual environment and install packages

A virtual environment isolates your project's packages from the rest of your system. This prevents version conflicts between different Python projects on your machine.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (backtick key, top-left of keyboard).
  • Create a virtual environment by running this command:
python -m venv mt5bot
  • Activate the virtual environment by running:
mt5bot\Scripts\activate

You should see (mt5bot) appear at the start of your terminal prompt. This confirms the environment is active.

Seeing a scripts disabled error?

Windows PowerShell may block the activation script by default. Run this command first to allow scripts in your current terminal session:

Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass

Then try the activate command again. This change only affects the current terminal window and resets when you close it.

Help me fix the PowerShell execution policy error

  • Install the required packages by running:
pip install MetaTrader5==5.0.5735 pandas pytz

What are these packages?

MetaTrader5 is the official Python package by MetaQuotes that communicates with the MT5 terminal via IPC.

pandas provides DataFrames for working with price data. pytz handles timezone conversions for market timestamps.

You should see a success message showing all packages installed. Confirm by checking for Successfully installed at the end of the output.

pip install failed?

Make sure you see (mt5bot) in your terminal prompt. If not, the virtual environment is not active and pip may install to the wrong location.

Also confirm that MetaTrader 5 terminal is running. The MetaTrader5 Python package requires the terminal to be installed on your machine.

Help me troubleshoot my pip install failure

Verify the MT5 connection

Before building the full bot, you need to confirm that Python can talk to the MetaTrader 5 terminal. A quick test script will validate the connection.

  • In VS Code, create a new file called test_connection.py in your project folder.
  • Make sure MetaTrader 5 is still running in the background.
  • Add the following code to test_connection.py:
import MetaTrader5 as mt5

mt5.initialize()
print(mt5.version())
print(mt5.account_info())
mt5.shutdown()

What does this code do?

mt5.initialize() opens a connection to the running MetaTrader 5 terminal. mt5.version() returns the terminal build number to prove the connection is live.

mt5.account_info() retrieves your demo account details including login number, balance, and server name. mt5.shutdown() cleanly closes the connection when the script finishes.

  • Save test_connection.py.
  • Run the script by executing this command in your terminal:
python test_connection.py

You should see your MT5 version tuple and a named tuple containing your demo account login, balance, and server name printed in the terminal.

Seeing None in the output?

If mt5.account_info() prints None, the terminal is not logged in. Switch to the MetaTrader 5 window and check that you are connected to your demo account (look for your account number in the top-left corner of the terminal).

If mt5.initialize() itself returns False, make sure the MetaTrader 5 terminal is open and running before you execute the script.

Help me fix my MT5 connection test

✔️ Awesome, I've got everything!

Great. Double check you have saved test_connection.py and confirmed your account details printed correctly.

ⓧ I'd like to double check the full code

Here is the complete test_connection.py file for comparison:

import MetaTrader5 as mt5

mt5.initialize()
print(mt5.version())
print(mt5.account_info())
mt5.shutdown()

Your environment is ready and Python is talking to MetaTrader 5. Next up, you will retrieve live market data and build the trading strategy that generates buy and sell signals.

Retrieve Market Data and Calculate Trading Signals

Your MT5 connection is live and verified. Now it's time to give your bot a brain.

A trading bot needs two things: a set of rules to follow and live market data to apply those rules against. In this step, you'll build a configuration module to hold your trading parameters, then create a strategy module that pulls price data from MetaTrader 5 and generates buy or sell signals using a moving average crossover technique.

In this step, get ready to:
  • Create a configuration file with all trading parameters.
  • Build a strategy module that calculates moving average crossover signals.
  • Test the strategy with live EURUSD data from your demo account.
Create the configuration file

Before writing any trading logic, your bot needs a single place to store all its settings. Centralizing parameters in one file means you can tune your strategy without hunting through multiple scripts.

  • In VS Code, create a new file called config.py in the same folder as your test_connection.py file.
  • Add the following code to config.py:
import MetaTrader5 as mt5

# Connection settings
MT5_LOGIN = 0  # Replace with your demo account login
MT5_PASSWORD = ""  # Replace with your demo account password
MT5_SERVER = "MetaQuotes-Demo"  # Replace with your broker's server

# Trading parameters
SYMBOL = "EURUSD"
TIMEFRAME = mt5.TIMEFRAME_H1
FAST_MA_PERIOD = 10
SLOW_MA_PERIOD = 20
LOT_SIZE = 0.1
SL_POINTS = 100
TP_POINTS = 200
DEVIATION = 20
MAGIC_NUMBER = 234000

# Bot settings
CHECK_INTERVAL_SECONDS = 60
LOG_FILE = "trading_bot.log"

What does this code do?

This file defines three groups of settings your bot will use throughout the project:

  • Connection settings store your demo account credentials so the bot can log in to MT5.
  • Trading parameters define the strategy rules: which currency pair to trade (EURUSD), the timeframe (1-hour bars), MA periods for signal calculation, position sizing, and risk levels.
  • Bot settings control how often the bot checks for new signals (every 60 seconds) and where logs are saved.
  • MAGIC_NUMBER is a unique ID that tags every trade your bot places. This lets you distinguish bot trades from manual trades in the same account.
  • Replace the MT5_LOGIN value with your demo account login number (the number you recorded when you created your demo account).
  • Replace the MT5_PASSWORD value with your demo account password.
  • Replace the MT5_SERVER value with your server name if it differs from MetaQuotes-Demo.
  • Save config.py.

Not sure what your login number is?

In the MetaTrader 5 terminal, look at the top of the Navigator panel on the left. Your account number appears next to the server name. You can also find it under File > Open an Account in your existing accounts list.

help me find my MT5 demo account login number and server name

Build the strategy module

The strategy module is where your bot makes decisions. It pulls recent price bars from MT5, calculates two moving averages (a fast one and a slow one), and detects when they cross. A crossover is the signal to trade.

What is a moving average crossover?

A moving average smooths out price noise by averaging closing prices over a set number of bars. A "fast" MA uses fewer bars (10 in this project) and reacts quickly to price changes. A "slow" MA uses more bars (20) and moves more gradually.

When the fast MA crosses above the slow MA, it suggests upward momentum (a BUY signal). When the fast MA crosses below, it suggests downward momentum (a SELL signal). If there's no crossover, the bot holds.

  • Create a new file called strategy.py in the same folder.
  • Add the following code to strategy.py:
import MetaTrader5 as mt5
import pandas as pd
from config import SYMBOL, TIMEFRAME, FAST_MA_PERIOD, SLOW_MA_PERIOD
import logging

logger = logging.getLogger(__name__)


def get_signal():
    """Retrieve market data and calculate MA crossover signal."""
    bars_needed = SLOW_MA_PERIOD + 2
    rates = mt5.copy_rates_from_pos(SYMBOL, TIMEFRAME, 0, bars_needed)

    if rates is None or len(rates) < bars_needed:
        logger.warning("Failed to retrieve rates for %s", SYMBOL)
        return "HOLD"

    df = pd.DataFrame(rates)
    df["fast_ma"] = df["close"].rolling(window=FAST_MA_PERIOD).mean()
    df["slow_ma"] = df["close"].rolling(window=SLOW_MA_PERIOD).mean()

    current_fast = df["fast_ma"].iloc[-1]
    current_slow = df["slow_ma"].iloc[-1]
    prev_fast = df["fast_ma"].iloc[-2]
    prev_slow = df["slow_ma"].iloc[-2]

    logger.info(
        "Price=%.5f | Fast MA=%.5f | Slow MA=%.5f",
        df["close"].iloc[-1],
        current_fast,
        current_slow,
    )

    if prev_fast <= prev_slow and current_fast > current_slow:
        logger.info("BUY signal detected: Fast MA crossed above Slow MA")
        return "BUY"
    elif prev_fast >= prev_slow and current_fast < current_slow:
        logger.info("SELL signal detected: Fast MA crossed below Slow MA")
        return "SELL"

    return "HOLD"

What does this code do?

The get_signal() function is the core decision engine of your bot:

  • mt5.copy_rates_from_pos() fetches the most recent price bars from the MT5 terminal. Position 0 means "start from the current bar" and it grabs 22 bars (SLOW_MA_PERIOD + 2) to have enough data for the rolling calculations.
  • The data is converted to a pandas DataFrame, then .rolling().mean() calculates the simple moving average for both the fast (10-bar) and slow (20-bar) windows.
  • Crossover detection compares the current bar's MA relationship against the previous bar's. If the fast MA was below the slow MA on the previous bar but is now above it, that's a BUY crossover.
  • If no crossover happened, the function returns "HOLD" so the bot takes no action.
  • Save strategy.py.

Seeing a ModuleNotFoundError for config?

Make sure config.py and strategy.py are in the same folder. Python can only import config if both files share the same directory.

help me fix a ModuleNotFoundError when importing config in my MT5 trading bot

Test the strategy with live data

Before integrating the strategy into the full bot, you should verify it can pull real price data and produce a signal. This quick test confirms everything works end-to-end.

  • Create a new file called test_strategy.py and add the following code:
import logging
import MetaTrader5 as mt5
from strategy import get_signal

# Set up logging so we can see the strategy's output
logging.basicConfig(level=logging.INFO, format="%(message)s")

# Initialize MT5 connection
mt5.initialize()

# Get the current trading signal
signal = get_signal()
print(f"\nCurrent signal: {signal}")

# Clean up
mt5.shutdown()
  • Save test_strategy.py.
  • Run the test script in your terminal by running:
python test_strategy.py

You should see output showing the current EURUSD price and both MA values, followed by the signal (BUY, SELL, or HOLD).

What should I see?

The output will look something like this (your numbers will differ based on live market data):

Price=1.08432 | Fast MA=1.08415 | Slow MA=1.08390

Current signal: HOLD

The signal depends on current market conditions. Seeing HOLD is the most common result because crossovers only happen occasionally on 1-hour bars. Any of BUY, SELL, or HOLD means your strategy is working correctly.

Seeing None or a warning about failed rates?

Make sure the MetaTrader 5 terminal is running and logged into your demo account. The Python package communicates with the desktop app via IPC, so both must be open simultaneously.

Also confirm EURUSD is visible in your MT5 Market Watch panel. Right-click in the Market Watch and select Show All if the symbol is missing.

help me debug why my MT5 strategy is returning None or failing to retrieve rate data

✔️ Awesome, I've got everything!

Great. Double check you've saved both config.py and strategy.py.

ⓧ I'd like to double check the full code

Here is the complete config.py:

import MetaTrader5 as mt5

# Connection settings
MT5_LOGIN = 0  # Replace with your demo account login
MT5_PASSWORD = ""  # Replace with your demo account password
MT5_SERVER = "MetaQuotes-Demo"  # Replace with your broker's server

# Trading parameters
SYMBOL = "EURUSD"
TIMEFRAME = mt5.TIMEFRAME_H1
FAST_MA_PERIOD = 10
SLOW_MA_PERIOD = 20
LOT_SIZE = 0.1
SL_POINTS = 100
TP_POINTS = 200
DEVIATION = 20
MAGIC_NUMBER = 234000

# Bot settings
CHECK_INTERVAL_SECONDS = 60
LOG_FILE = "trading_bot.log"

Here is the complete strategy.py:

import MetaTrader5 as mt5
import pandas as pd
from config import SYMBOL, TIMEFRAME, FAST_MA_PERIOD, SLOW_MA_PERIOD
import logging

logger = logging.getLogger(__name__)


def get_signal():
    """Retrieve market data and calculate MA crossover signal."""
    bars_needed = SLOW_MA_PERIOD + 2
    rates = mt5.copy_rates_from_pos(SYMBOL, TIMEFRAME, 0, bars_needed)

    if rates is None or len(rates) < bars_needed:
        logger.warning("Failed to retrieve rates for %s", SYMBOL)
        return "HOLD"

    df = pd.DataFrame(rates)
    df["fast_ma"] = df["close"].rolling(window=FAST_MA_PERIOD).mean()
    df["slow_ma"] = df["close"].rolling(window=SLOW_MA_PERIOD).mean()

    current_fast = df["fast_ma"].iloc[-1]
    current_slow = df["slow_ma"].iloc[-1]
    prev_fast = df["fast_ma"].iloc[-2]
    prev_slow = df["slow_ma"].iloc[-2]

    logger.info(
        "Price=%.5f | Fast MA=%.5f | Slow MA=%.5f",
        df["close"].iloc[-1],
        current_fast,
        current_slow,
    )

    if prev_fast <= prev_slow and current_fast > current_slow:
        logger.info("BUY signal detected: Fast MA crossed above Slow MA")
        return "BUY"
    elif prev_fast >= prev_slow and current_fast < current_slow:
        logger.info("SELL signal detected: Fast MA crossed below Slow MA")
        return "SELL"

    return "HOLD"

Your bot now has a working strategy brain that reads live market data and generates trading signals. Next up, you'll build the execution engine that turns those signals into actual trades on your demo account.

Build the Order Execution Engine

Your strategy module can now read the market and decide when to buy or sell. The missing piece is a module that actually executes those decisions by sending orders to MetaTrader 5.

In this step, you will build an order execution engine that opens and closes trades, calculates stop-loss and take-profit levels for risk management, and logs every decision for full transparency.

In this step, get ready to:
  • Create position-checking functions that query open trades by magic number.
  • Build a trade execution function with stop-loss, take-profit, and order validation.
  • Add a close-position function and verify the full order lifecycle on your demo account.
Create the position-checking functions

Before the bot can open a new trade, it needs to know whether a trade is already open. Without this check, the bot could stack duplicate positions on every loop iteration and blow through the account balance.

  • Create a new file called executor.py in your project folder (the same folder as config.py and strategy.py).
  • Add the imports, logger setup, and position-checking functions by pasting this code:
import MetaTrader5 as mt5
from config import SYMBOL, LOT_SIZE, SL_POINTS, TP_POINTS, DEVIATION, MAGIC_NUMBER
import logging

logger = logging.getLogger(__name__)


def has_open_position():
    """Check if a position with our magic number already exists."""
    positions = mt5.positions_get(symbol=SYMBOL)
    if positions is None:
        return False
    for pos in positions:
        if pos.magic == MAGIC_NUMBER:
            return True
    return False


def get_open_position():
    """Get the current open position with our magic number."""
    positions = mt5.positions_get(symbol=SYMBOL)
    if positions is None:
        return None
    for pos in positions:
        if pos.magic == MAGIC_NUMBER:
            return pos
    return None

What does this code do?

These two helper functions both query MT5 for open positions filtered by your symbol and magic number.

  • has_open_position() returns True or False. The main loop uses this to avoid opening duplicate trades.
  • get_open_position() returns the actual position object (with its ticket, volume, and type) so you can close it later.
  • The MAGIC_NUMBER acts as a unique tag. It lets your bot identify only its own trades, even if other scripts or manual trades exist on the same account.

What is a magic number?

A magic number is an integer you assign to orders so your bot can distinguish its trades from others. Think of it like a name tag on a lunchbox in a shared fridge. Your config sets it to 234000, but any unique integer works.

  • Save executor.py.

You won't see a change yet. The next chunk builds the function that actually sends orders to MT5.

Build the trade execution function

The execution function is the core of your trading engine. It retrieves the current price, calculates stop-loss and take-profit levels for risk management, validates the order has sufficient funds, then sends it to the broker.

  • In executor.py, add the open_position() function below the get_open_position() function by pasting this code:
def open_position(direction):
    """Open a new position in the specified direction."""
    symbol_info = mt5.symbol_info(SYMBOL)
    if symbol_info is None:
        logger.error("Symbol %s not found", SYMBOL)
        return None

    point = symbol_info.point
    tick = mt5.symbol_info_tick(SYMBOL)
    if tick is None:
        logger.error("Failed to get tick for %s", SYMBOL)
        return None

    if direction == "BUY":
        order_type = mt5.ORDER_TYPE_BUY
        price = tick.ask
        sl = price - SL_POINTS * point
        tp = price + TP_POINTS * point
    elif direction == "SELL":
        order_type = mt5.ORDER_TYPE_SELL
        price = tick.bid
        sl = price + SL_POINTS * point
        tp = price - TP_POINTS * point
    else:
        logger.error("Invalid direction: %s", direction)
        return None

    request = {
        "action": mt5.TRADE_ACTION_DEAL,
        "symbol": SYMBOL,
        "volume": LOT_SIZE,
        "type": order_type,
        "price": price,
        "sl": sl,
        "tp": tp,
        "deviation": DEVIATION,
        "magic": MAGIC_NUMBER,
        "comment": "python_ma_bot",
        "type_time": mt5.ORDER_TIME_GTC,
        "type_filling": mt5.ORDER_FILLING_RETURN,
    }

    check_result = mt5.order_check(request)
    if check_result is None or check_result.retcode != 0:
        logger.error(
            "Order check failed: %s",
            check_result.comment if check_result else "None returned",
        )
        return None

    result = mt5.order_send(request)
    if result.retcode != mt5.TRADE_RETCODE_DONE:
        logger.error("Order send failed: retcode=%d", result.retcode)
        return None

    logger.info(
        "Position opened: %s %s %.2f lots at %.5f | SL=%.5f | TP=%.5f | Ticket=%d",
        direction,
        SYMBOL,
        LOT_SIZE,
        result.price,
        sl,
        tp,
        result.order,
    )
    return result

What does this code do?

This function handles the full lifecycle of opening a trade. It has three main stages:

  • Price and risk calculation: mt5.symbol_info() retrieves the symbol's point value (the minimum price movement). The stop-loss and take-profit are calculated by multiplying SL_POINTS and TP_POINTS by this point value, then adding or subtracting from the entry price.
  • Order validation: mt5.order_check() verifies the account has enough margin to cover the trade before sending it. This returns retcode 0 on success.
  • Execution: mt5.order_send() submits the order to the broker. A retcode of 10009 (TRADE_RETCODE_DONE) means the trade executed successfully.

Why ORDER_FILLING_RETURN?

ORDER_FILLING_RETURN is the safest filling type for market orders. If the full volume cannot be filled immediately, the remaining volume stays as an active order. It works with both Market and Exchange execution modes, making it the most compatible choice across brokers.

  • Save executor.py.

You won't see a change yet. The next chunk adds the closing function, then you will test the full executor.

Close positions and test the executor

To close a position in MT5, you send a reverse trade (sell to close a buy, buy to close a sell) and include the original position's ticket number so the broker knows which trade to close.

  • In executor.py, add the close_position() function below open_position() by pasting this code:
def close_position(position):
    """Close an existing position."""
    tick = mt5.symbol_info_tick(SYMBOL)
    if tick is None:
        logger.error("Failed to get tick for closing position")
        return None

    if position.type == mt5.ORDER_TYPE_BUY:
        order_type = mt5.ORDER_TYPE_SELL
        price = tick.bid
    else:
        order_type = mt5.ORDER_TYPE_BUY
        price = tick.ask

    request = {
        "action": mt5.TRADE_ACTION_DEAL,
        "symbol": SYMBOL,
        "volume": position.volume,
        "type": order_type,
        "position": position.ticket,
        "price": price,
        "deviation": DEVIATION,
        "magic": MAGIC_NUMBER,
        "comment": "python_ma_bot_close",
        "type_time": mt5.ORDER_TIME_GTC,
        "type_filling": mt5.ORDER_FILLING_RETURN,
    }

    result = mt5.order_send(request)
    if result.retcode != mt5.TRADE_RETCODE_DONE:
        logger.error("Close position failed: retcode=%d", result.retcode)
        return None

    logger.info(
        "Position closed: Ticket=%d at %.5f | Profit=%.2f",
        position.ticket,
        result.price,
        position.profit,
    )
    return result

What does this code do?

  • It determines the reverse order type. A BUY position is closed with a SELL, and vice versa.
  • The position field in the request dict carries the original ticket number. This tells MT5 to close that specific trade rather than opening a brand new one.
  • On success, it logs the ticket, closing price, and profit/loss so you have a complete audit trail.
  • Save executor.py.

Now verify the executor works by manually triggering a trade on your demo account.

  • Create a test file called test_executor.py and paste the following code:
import MetaTrader5 as mt5
import logging
from config import MT5_LOGIN, MT5_PASSWORD, MT5_SERVER, SYMBOL
from executor import open_position, has_open_position, get_open_position, close_position

# Set up logging so you can see executor output
logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")

# Connect to MT5
if not mt5.initialize(login=MT5_LOGIN, server=MT5_SERVER, password=MT5_PASSWORD):
    print("MT5 init failed:", mt5.last_error())
    quit()

mt5.symbol_select(SYMBOL, True)

# Test opening a position
print("Has open position:", has_open_position())
result = open_position("BUY")
print("Order result:", result)
print("Has open position now:", has_open_position())

# Close the position we just opened
position = get_open_position()
if position:
    close_position(position)
    print("Position closed.")

mt5.shutdown()
  • Make sure the MetaTrader 5 terminal is open and logged in to your demo account.
  • Run the test script by executing this command in your terminal:
python test_executor.py
  • In the MT5 terminal, click the **Trade** tab at the bottom of the screen.
  • You should see your BUY position appear briefly (with the comment python_ma_bot), then disappear as the script closes it.
  • In your VS Code terminal, confirm you see log lines showing the full order lifecycle: position opened, then position closed with a profit/loss value.

Order check failed or order send failed?

  • Make sure the MT5 terminal is open and connected (green bar in the bottom-right corner). The Python package communicates with the terminal via IPC, so the terminal must be running.
  • Check that EURUSD is visible in MarketWatch (the left panel in MT5). If it is not there, right-click in MarketWatch, select **Symbols**, find EURUSD, and click **Show**.
  • Verify your demo account credentials in config.py match what you see in MT5 under **File > Open an Account**.

Help me debug my MT5 order execution error

✔️ Awesome, I've got everything!

Great. Double check you have saved executor.py and confirmed a trade appeared in the MT5 Trade tab.

ⓧ I'd like to double check the full code

Here is the complete executor.py file. Compare yours against it:

import MetaTrader5 as mt5
from config import SYMBOL, LOT_SIZE, SL_POINTS, TP_POINTS, DEVIATION, MAGIC_NUMBER
import logging

logger = logging.getLogger(__name__)


def has_open_position():
    """Check if a position with our magic number already exists."""
    positions = mt5.positions_get(symbol=SYMBOL)
    if positions is None:
        return False
    for pos in positions:
        if pos.magic == MAGIC_NUMBER:
            return True
    return False


def get_open_position():
    """Get the current open position with our magic number."""
    positions = mt5.positions_get(symbol=SYMBOL)
    if positions is None:
        return None
    for pos in positions:
        if pos.magic == MAGIC_NUMBER:
            return pos
    return None


def open_position(direction):
    """Open a new position in the specified direction."""
    symbol_info = mt5.symbol_info(SYMBOL)
    if symbol_info is None:
        logger.error("Symbol %s not found", SYMBOL)
        return None

    point = symbol_info.point
    tick = mt5.symbol_info_tick(SYMBOL)
    if tick is None:
        logger.error("Failed to get tick for %s", SYMBOL)
        return None

    if direction == "BUY":
        order_type = mt5.ORDER_TYPE_BUY
        price = tick.ask
        sl = price - SL_POINTS * point
        tp = price + TP_POINTS * point
    elif direction == "SELL":
        order_type = mt5.ORDER_TYPE_SELL
        price = tick.bid
        sl = price + SL_POINTS * point
        tp = price - TP_POINTS * point
    else:
        logger.error("Invalid direction: %s", direction)
        return None

    request = {
        "action": mt5.TRADE_ACTION_DEAL,
        "symbol": SYMBOL,
        "volume": LOT_SIZE,
        "type": order_type,
        "price": price,
        "sl": sl,
        "tp": tp,
        "deviation": DEVIATION,
        "magic": MAGIC_NUMBER,
        "comment": "python_ma_bot",
        "type_time": mt5.ORDER_TIME_GTC,
        "type_filling": mt5.ORDER_FILLING_RETURN,
    }

    check_result = mt5.order_check(request)
    if check_result is None or check_result.retcode != 0:
        logger.error(
            "Order check failed: %s",
            check_result.comment if check_result else "None returned",
        )
        return None

    result = mt5.order_send(request)
    if result.retcode != mt5.TRADE_RETCODE_DONE:
        logger.error("Order send failed: retcode=%d", result.retcode)
        return None

    logger.info(
        "Position opened: %s %s %.2f lots at %.5f | SL=%.5f | TP=%.5f | Ticket=%d",
        direction,
        SYMBOL,
        LOT_SIZE,
        result.price,
        sl,
        tp,
        result.order,
    )
    return result


def close_position(position):
    """Close an existing position."""
    tick = mt5.symbol_info_tick(SYMBOL)
    if tick is None:
        logger.error("Failed to get tick for closing position")
        return None

    if position.type == mt5.ORDER_TYPE_BUY:
        order_type = mt5.ORDER_TYPE_SELL
        price = tick.bid
    else:
        order_type = mt5.ORDER_TYPE_BUY
        price = tick.ask

    request = {
        "action": mt5.TRADE_ACTION_DEAL,
        "symbol": SYMBOL,
        "volume": position.volume,
        "type": order_type,
        "position": position.ticket,
        "price": price,
        "deviation": DEVIATION,
        "magic": MAGIC_NUMBER,
        "comment": "python_ma_bot_close",
        "type_time": mt5.ORDER_TIME_GTC,
        "type_filling": mt5.ORDER_FILLING_RETURN,
    }

    result = mt5.order_send(request)
    if result.retcode != mt5.TRADE_RETCODE_DONE:
        logger.error("Close position failed: retcode=%d", result.retcode)
        return None

    logger.info(
        "Position closed: Ticket=%d at %.5f | Profit=%.2f",
        position.ticket,
        result.price,
        position.profit,
    )
    return result

Your execution engine is fully operational. You can open trades, manage risk with stop-loss and take-profit, and close positions programmatically. Next up, you will wire everything together into an automated loop that checks for signals and trades without any manual intervention.

Assemble the Main Trading Loop

Your strategy module knows how to read the market, and your executor knows how to place trades. Now you need an orchestrator that ties everything together into a continuously running program.

In this step, you will build bot.py, the main entry point that initializes the MT5 connection, checks for trading signals on a timer, executes trades through your executor, and shuts down cleanly when you stop it.

In this step, get ready to:
  • Create the bot entry point with structured logging and MT5 initialization.
  • Build a main loop that checks signals every 60 seconds and executes trades.
  • Implement graceful shutdown so the bot exits cleanly on Ctrl+C.
Create the bot entry point with logging and initialization

A production bot needs visibility into what it is doing. Structured logging writes timestamped records to both the terminal and a file, so you can monitor live decisions and review historical trades later.

  • Create a new file called bot.py in the same folder as your other modules (config.py, strategy.py, executor.py).
  • Add the imports, logging setup, and MT5 initialization function by pasting this code:
import time
import logging
import MetaTrader5 as mt5
from config import (
    MT5_LOGIN,
    MT5_PASSWORD,
    MT5_SERVER,
    SYMBOL,
    CHECK_INTERVAL_SECONDS,
    LOG_FILE,
)
from strategy import get_signal
from executor import has_open_position, get_open_position, open_position, close_position


def setup_logging():
    """Configure logging with file and console handlers."""
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
        handlers=[
            logging.FileHandler(LOG_FILE),
            logging.StreamHandler(),
        ],
    )


def initialize_mt5():
    """Connect to MetaTrader 5 terminal."""
    logger = logging.getLogger(__name__)

    if not mt5.initialize(login=MT5_LOGIN, server=MT5_SERVER, password=MT5_PASSWORD):
        logger.error("MT5 initialize() failed, error: %s", mt5.last_error())
        return False

    account_info = mt5.account_info()
    if account_info is None:
        logger.error("Failed to get account info")
        mt5.shutdown()
        return False

    logger.info(
        "Connected to %s | Account: %d | Balance: %.2f %s",
        account_info.server,
        account_info.login,
        account_info.balance,
        account_info.currency,
    )

    if not mt5.symbol_select(SYMBOL, True):
        logger.error("Failed to select symbol %s", SYMBOL)
        mt5.shutdown()
        return False

    return True

What does this code do?

This code sets up two foundational pieces of the bot:

  • setup_logging() configures Python's logging to write every message both to the terminal (StreamHandler) and to a file called trading_bot.log (FileHandler). Each log line includes a timestamp, module name, severity level, and the message.
  • initialize_mt5() connects to the MT5 terminal with your credentials, verifies the account is reachable, logs your balance, and enables the EURUSD symbol in MarketWatch with mt5.symbol_select(). If any step fails, it cleans up and returns False.
  • Save bot.py.
  • You won't see any output yet. The next chunk adds the main loop that drives the bot.
Build the main trading loop

The bot needs to repeatedly wake up, check for a trading signal, decide whether to act, then go back to sleep. This is an event-driven architecture pattern where the "event" is the timer firing every 60 seconds.

  • In bot.py, add the run_bot() function below initialize_mt5() by pasting this code:
def run_bot():
    """Main trading loop."""
    logger = logging.getLogger(__name__)
    logger.info("Trading bot started. Checking every %d seconds.", CHECK_INTERVAL_SECONDS)

    while True:
        signal = get_signal()

        if signal == "HOLD":
            logger.info("Signal: HOLD. No action taken.")
        elif signal in ("BUY", "SELL"):
            position = get_open_position()

            if position is not None:
                if (signal == "BUY" and position.type == mt5.ORDER_TYPE_SELL) or (
                    signal == "SELL" and position.type == mt5.ORDER_TYPE_BUY
                ):
                    logger.info("Closing opposing position before opening new one")
                    close_position(position)
                    open_position(signal)
                else:
                    logger.info("Position already open in same direction. Holding.")
            else:
                open_position(signal)

        time.sleep(CHECK_INTERVAL_SECONDS)

What does this loop do?

  • get_signal() fetches the latest price bars and returns BUY, SELL, or HOLD based on the moving average crossover.
  • If the signal is BUY or SELL, the loop checks whether a position is already open. If an opposing position exists (e.g. you hold a SELL but the signal says BUY), it closes the old one first, then opens the new direction.
  • time.sleep(CHECK_INTERVAL_SECONDS) pauses the loop for 60 seconds before checking again. This prevents the bot from hammering the MT5 API.

Why close opposing positions?

If the market reverses direction, holding both a BUY and a SELL would cancel out your gains. Closing the old position before opening a new one keeps your exposure clean and aligned with the strategy's current signal.

  • Save bot.py.
  • You won't see a change yet. The next chunk adds the main() function that starts everything.
Implement graceful shutdown and run the bot

A trading bot that crashes without closing its MT5 connection can leave stale resources behind. Wrapping the loop in a try/except for KeyboardInterrupt ensures the bot always calls mt5.shutdown() before exiting.

  • Add the main() function and the entry-point guard at the bottom of bot.py by pasting this code below run_bot():
def main():
    setup_logging()
    logger = logging.getLogger(__name__)

    logger.info("=" * 60)
    logger.info("MT5 TRADING BOT - Starting")
    logger.info("=" * 60)

    if not initialize_mt5():
        logger.critical("Failed to initialize MT5. Exiting.")
        return

    try:
        run_bot()
    except KeyboardInterrupt:
        logger.info("Keyboard interrupt received. Shutting down gracefully.")
    finally:
        mt5.shutdown()
        logger.info("MT5 connection closed. Bot stopped.")


if __name__ == "__main__":
    main()

What does this code do?

  • main() orchestrates the startup sequence: configure logging first, attempt MT5 initialization, then hand off to run_bot().
  • The try/except KeyboardInterrupt catches Ctrl+C so the bot logs a clean shutdown message instead of dumping a traceback.
  • The finally block guarantees mt5.shutdown() runs no matter how the loop exits. This closes the IPC connection to the MT5 terminal.
  • if __name__ == "__main__" ensures main() only runs when you execute the file directly, not when other modules import from it.
  • Save bot.py.
  • Make sure the MetaTrader 5 terminal is running and logged in to your demo account.
  • Run the bot by executing this command in your terminal:
python bot.py

You should see log output in your terminal showing the bot connecting to your account, then checking for signals every 60 seconds. Each cycle prints the current price, both MA values, and the resulting signal (BUY, SELL, or HOLD).

  • Let the bot run for 2-3 cycles to confirm it logs signals repeatedly.
  • Press Ctrl+C to stop the bot.
  • Confirm the log shows "Keyboard interrupt received. Shutting down gracefully." followed by "MT5 connection closed. Bot stopped."

Where is the log file?

A file called trading_bot.log now exists in the same folder as bot.py. It contains the same messages you saw in the terminal. You can open it in VS Code to review past bot activity even after the bot stops.

Bot crashes on startup?

  • If you see "MT5 initialize() failed", make sure the MetaTrader 5 terminal is open and logged in. The Python package communicates with the running terminal via IPC.
  • If you see "Failed to select symbol EURUSD", open the MT5 terminal, right-click in the Market Watch panel, and select Show All. Then run the bot again.
  • If you see a ModuleNotFoundError, confirm your virtual environment is activated. You should see (mt5bot) at the start of your terminal prompt.

Help me debug my MT5 trading bot startup error

✔️ Awesome, I've got everything!

Great. Double check you have saved bot.py and confirmed the bot logs signals and shuts down cleanly.

ⓧ I'd like to double check the full code

import time
import logging
import MetaTrader5 as mt5
from config import (
    MT5_LOGIN,
    MT5_PASSWORD,
    MT5_SERVER,
    SYMBOL,
    CHECK_INTERVAL_SECONDS,
    LOG_FILE,
)
from strategy import get_signal
from executor import has_open_position, get_open_position, open_position, close_position


def setup_logging():
    """Configure logging with file and console handlers."""
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s | %(name)s | %(levelname)s | %(message)s",
        handlers=[
            logging.FileHandler(LOG_FILE),
            logging.StreamHandler(),
        ],
    )


def initialize_mt5():
    """Connect to MetaTrader 5 terminal."""
    logger = logging.getLogger(__name__)

    if not mt5.initialize(login=MT5_LOGIN, server=MT5_SERVER, password=MT5_PASSWORD):
        logger.error("MT5 initialize() failed, error: %s", mt5.last_error())
        return False

    account_info = mt5.account_info()
    if account_info is None:
        logger.error("Failed to get account info")
        mt5.shutdown()
        return False

    logger.info(
        "Connected to %s | Account: %d | Balance: %.2f %s",
        account_info.server,
        account_info.login,
        account_info.balance,
        account_info.currency,
    )

    if not mt5.symbol_select(SYMBOL, True):
        logger.error("Failed to select symbol %s", SYMBOL)
        mt5.shutdown()
        return False

    return True


def run_bot():
    """Main trading loop."""
    logger = logging.getLogger(__name__)
    logger.info("Trading bot started. Checking every %d seconds.", CHECK_INTERVAL_SECONDS)

    while True:
        signal = get_signal()

        if signal == "HOLD":
            logger.info("Signal: HOLD. No action taken.")
        elif signal in ("BUY", "SELL"):
            position = get_open_position()

            if position is not None:
                if (signal == "BUY" and position.type == mt5.ORDER_TYPE_SELL) or (
                    signal == "SELL" and position.type == mt5.ORDER_TYPE_BUY
                ):
                    logger.info("Closing opposing position before opening new one")
                    close_position(position)
                    open_position(signal)
                else:
                    logger.info("Position already open in same direction. Holding.")
            else:
                open_position(signal)

        time.sleep(CHECK_INTERVAL_SECONDS)


def main():
    setup_logging()
    logger = logging.getLogger(__name__)

    logger.info("=" * 60)
    logger.info("MT5 TRADING BOT - Starting")
    logger.info("=" * 60)

    if not initialize_mt5():
        logger.critical("Failed to initialize MT5. Exiting.")
        return

    try:
        run_bot()
    except KeyboardInterrupt:
        logger.info("Keyboard interrupt received. Shutting down gracefully.")
    finally:
        mt5.shutdown()
        logger.info("MT5 connection closed. Bot stopped.")


if __name__ == "__main__":
    main()

Your trading bot is fully assembled and running on live market data. Next up, you will push the bot further by adding reconnection logic so it can survive disconnections without your intervention.

Secret mission

Add Reconnection Logic and Error Recovery

Your trading bot works, but what happens when MetaTrader 5 crashes or loses its network connection? In production, bots need to survive failures without human intervention. Build automatic reconnection with exponential backoff and error recovery so your bot can handle the real world.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally using a free demo account with virtual money, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • MetaTrader 5 Terminal (installed Windows application).
  • Python virtual environment mt5bot with installed packages (local disk space).
  • Project files: config.py, strategy.py, executor.py, bot.py, test_connection.py.
  • Log file trading_bot.log generated by the bot.

What about your demo account?

Your MetaQuotes-Demo account is free and uses only virtual money. It expires automatically after a period of inactivity, so there is nothing to delete or cancel.

Keep everything running

No action needed. Choose this if you are still actively building or want to experiment with different strategies on your demo account.

  • Leave MetaTrader 5 installed and your demo account open for further experimentation.
  • Keep your virtual environment and project files intact so you can return anytime.
  • Your demo account uses virtual money, so there is zero financial risk in keeping everything running.

Pause - I'll come back to this later

Shut down running processes to free up system resources. Everything stays installed on disk for later use.

  • Stop the bot if it is still running by pressing Ctrl+C in your terminal.
  • Close the MetaTrader 5 terminal window.
  • Deactivate the virtual environment by running:
deactivate
  • Your project files, virtual environment, and MT5 installation remain on disk ready for when you return.

Delete - I don't want to use this again

Remove all project resources and the MetaTrader 5 installation from your machine.

  • Stop the bot if it is still running by pressing Ctrl+C in your terminal.
  • Deactivate the virtual environment by running:
deactivate
  • Delete the virtual environment folder by running:
rmdir /s /q mt5bot
  • Delete the project files and log file by running:
del config.py strategy.py executor.py bot.py test_connection.py trading_bot.log
  • Uninstall MetaTrader 5: open the Start menu, find the MetaTrader 5 program group, and select Uninstall.
  • In the uninstall wizard, enable Delete user personal data if you want to remove all historical data and platform settings.
  • Click Next and wait for the uninstallation to complete.

That's a Wrap!

That's a Wrap!

Nice work! You just built a complete automated trading bot that connects to MetaTrader 5, retrieves live market data, and executes trades on a demo account using a moving average crossover strategy.

You've learned how to:

  • Built a Python trading bot that connects to MetaTrader 5 via its Python API, retrieves live market data, and calculates moving average crossover signals using pandas.
  • Executed trades programmatically with stop-loss, take-profit, and position sizing through the MT5 order execution API.
  • Implemented production-style structured logging with file and console output, plus a graceful shutdown mechanism for clean process termination.
  • Secret Mission: Added automatic reconnection with exponential backoff and error recovery, making the bot resilient to MT5 disconnections without manual intervention.

Ready to quiz yourself?