Telegram-to-MT5 Signal Copier Bot

Build a Python bot that reads Telegram signals and auto-executes MT5 trades.

Introduction

30 Second Summary

Trading signals drop in a Telegram channel at all hours. Manually checking messages, switching to your trading terminal, and typing in the order means missed entries and slow execution.

In this project, you will build a Python bot that monitors a Telegram channel for trading signals, parses them into structured trade parameters, and automatically executes them on your MetaTrader 5 terminal.

What You'll Build

When a signal like "buy xauusd 7896, tp1 8000, sl 7880" appears in your channel, your bot reads it within seconds, extracts the trade details, and opens the position on your AquaFunded account with the correct stop-loss and take-profit.

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

  • A real-time Telegram listener running on your personal account that catches new trading signals the instant they appear in your channel, printing each one to your console so you can watch it work.
  • A regex-based signal parser that handles messy formatting variations and correctly extracts direction, symbol, entry price, stop-loss, and take-profit from raw message text.
  • Automated MT5 trade execution where you see your terminal open positions the moment a valid signal is detected, complete with the exact SL and TP from the message.
  • Secret Mission: Dynamic lot sizing that calculates position size based on your account balance and SL distance, so you never risk more than a fixed percentage per trade.

Are there any prerequisites?

This project requires Python 3.13, VS Code, and the MetaTrader 5 terminal installed and logged in to your AquaFunded account. You also need a Telegram account that is already a member of the signal channel you want to copy from.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters for your trading workflow.

You're going to build a Python bot that monitors a Telegram signal channel in real-time and automatically fires off trades on your MetaTrader 5 terminal. No more staring at your phone waiting for signals to drop.

Set Up Your Environment and Get Telegram API Credentials

Your signal copier bot needs two things before it can do anything: credentials to authenticate with Telegram's API as your personal account, and the Python libraries that talk to Telegram and MetaTrader 5.

In this step, you'll register a Telegram app to get your API credentials, install the packages, and create a config file that keeps everything in one place.

In this step, get ready to:
  • Register your Telegram app and get your API credentials.
  • Install the Telethon and MetaTrader5 Python packages.
  • Create your project folder and config file.
Register your app on my.telegram.org

The Telegram API uses an api_id and api_hash pair to identify your application. These let your Python script act as your personal Telegram account, reading messages from any channel you are subscribed to.

  • Open your browser and navigate to https://my.telegram.org.
  • Log in with the phone number linked to your Telegram account.
  • Telegram will send a verification code to your Telegram app. Enter the code on the website to continue.
  • Click API development tools.
  • Fill in the App title field (e.g. Signal Copier) and the Short name field (e.g. signalcopier). Leave the URL field empty.
  • Click Create application.
  • You will see your App api_id (a number) and App api_hash (a long hex string). Copy both values somewhere safe. You will need them in a moment.

Keep your api_hash secret

Telegram will not let you revoke your api_hash once created. Never share it publicly or commit it to a public repository.

What are api_id and api_hash?

These credentials identify your application to Telegram's servers. Unlike a bot token, they let your script log in as your personal account (a "userbot"). This means you can read messages from any channel you have joined, without needing admin access.

Install the required Python packages

Your bot depends on two libraries: Telethon for connecting to Telegram, and the MetaTrader5 package for talking to your MT5 terminal.

  • Open your terminal. Press the Windows key, type cmd, and press Enter.
  • Install all three packages by running this command:
pip install telethon MetaTrader5 numpy==1.26.4

What does this command do?

  • telethon is the library that connects to Telegram using your personal account credentials.
  • MetaTrader5 is the official Python package for sending trade orders to your MT5 terminal.
  • numpy==1.26.4 pins numpy to a version compatible with the MetaTrader5 package. Newer numpy versions can cause import errors.

You should see pip downloading and installing the packages. The output ends with a line confirming the packages were successfully installed.

Seeing an error during install?

  • If you see a permissions error, try running pip install --user telethon MetaTrader5 numpy==1.26.4 instead.
  • If pip is not found, make sure Python is in your system PATH. Open a new terminal and try again.
  • help me fix a pip install error for telethon and MetaTrader5

Before you move on, what do you think will happen when you try to import these two libraries? Will it print something, or throw an error?

  • Verify the packages installed correctly by running:
python -c "import telethon; import MetaTrader5; print('All good')"

You should see All good printed with no errors. This confirms both libraries are importable.

Seeing an import error?

  • If you see an error mentioning numpy, run pip install numpy==1.26.4 again and retry.
  • If python is not recognized, try py instead. This is a common alias on Windows.
  • help me fix an import error with MetaTrader5 or telethon in Python
Create your project folder and config file

Now you need a dedicated folder for your bot's code and a config file to store all your credentials in one place.

  • Move to your Desktop by running this command:
cd %USERPROFILE%\Desktop
  • Create your project folder and move into it by running these commands:
mkdir signal-copier
cd signal-copier
  • Open this folder in VS Code by running:
code .

VS Code opens with your empty signal-copier folder in the sidebar.

VS Code didn't open?

If code is not recognized, open VS Code manually. Press the Windows key, type VS Code, and press Enter. Then click File, then Open Folder, and select the signal-copier folder on your Desktop.

  • In the VS Code Explorer sidebar, click the New File icon.
  • Name the file config.py and press Enter.
  • Paste the following code into config.py:
# Telegram API credentials (get from https://my.telegram.org)
API_ID = 12345678
API_HASH = "your-api-hash-here"
PHONE_NUMBER = "+234your-phone-number"

# Telegram channel (use the invite link or channel username)
CHANNEL = "https://t.me/+your-invite-link-here"

# MT5 settings
MT5_LOGIN = 12345678
MT5_PASSWORD = "your-mt5-password-here"
MT5_SERVER = "your-aquafunded-server-here"

# Trading settings
LOT_SIZE = 0.01
MAGIC_NUMBER = 123456
DEVIATION = 20

What does this config do?

  • API_ID and API_HASH authenticate your bot with Telegram as your personal account.
  • PHONE_NUMBER is the phone number registered on Telegram (used during first login).
  • CHANNEL is the invite link for the signal channel your bot will monitor.
  • MT5_LOGIN, MT5_PASSWORD, and MT5_SERVER are your AquaFunded MetaTrader 5 account details.
  • LOT_SIZE, MAGIC_NUMBER, and DEVIATION control trade size, trade identification, and price slippage tolerance.
  • Replace the placeholder values with your real credentials:
  • Set API_ID to the number you copied from my.telegram.org.
  • Set API_HASH to the hex string you copied from my.telegram.org.
  • Set PHONE_NUMBER to your Telegram phone number with country code (e.g. +2348012345678).
  • Set CHANNEL to the invite link of the signal channel you joined.
  • Set MT5_LOGIN to your AquaFunded MT5 account number.
  • Set MT5_PASSWORD to your MT5 account password.
  • Set MT5_SERVER to your MT5 server name (visible in the MT5 terminal title bar or under File, then Login to Trade Account).
  • Save config.py with Ctrl+S.

Leave LOT_SIZE, MAGIC_NUMBER, and DEVIATION as-is

These trading settings are already set to safe defaults. 0.01 is the smallest lot size, so your test trades carry minimal risk.

✔️ Awesome, I've got everything!

Great. Double check you have saved config.py with your real credentials filled in.

ⓧ I'd like to double check the full code

Your config.py should look like this (with your own values replacing the placeholders):

# Telegram API credentials (get from https://my.telegram.org)
API_ID = 12345678
API_HASH = "your-api-hash-here"
PHONE_NUMBER = "+234your-phone-number"

# Telegram channel (use the invite link or channel username)
CHANNEL = "https://t.me/+your-invite-link-here"

# MT5 settings
MT5_LOGIN = 12345678
MT5_PASSWORD = "your-mt5-password-here"
MT5_SERVER = "your-aquafunded-server-here"

# Trading settings
LOT_SIZE = 0.01
MAGIC_NUMBER = 123456
DEVIATION = 20

Now let's confirm everything still imports cleanly from inside your project folder. What do you expect to see when you run the import check again?

  • In the VS Code integrated terminal (open it with Ctrl+`), run:
python -c "import telethon; import MetaTrader5; print('All good')"

You should see All good printed. Your environment is ready and your credentials are stored.

Your environment is locked and loaded. Next up, you'll connect to Telegram and prove your bot can read live messages from the signal channel.

Connect to Telegram and Listen to the Signal Channel

Your credentials are locked in and packages are installed. Now you need to prove something critical: that your bot can connect to Telegram as your personal account and read messages from a channel where you are just a normal member.

Telethon uses the "userbot" approach. It logs in as YOU, not as a separate bot account. This means you can read any channel you have joined, no admin access needed. In this step, you will write a listener that authenticates with your phone number and prints every new channel message to the terminal.

In this step, get ready to:
  • Connect to Telegram using your API credentials and phone number.
  • Resolve the signal channel and register a message handler.
  • Run the bot and see live messages printing in your terminal.
Write the Telegram client connection

The first thing your bot needs to do is authenticate with Telegram. Telethon handles this with a one-time interactive login that creates a session file on disk. Future runs skip the login entirely because the session file remembers your credentials.

  • Create a new file called bot.py in your project folder (the same folder where config.py lives).
  • Add the Telegram connection code by pasting the following into bot.py:
import asyncio
from telethon import TelegramClient, events
from config import API_ID, API_HASH, PHONE_NUMBER, CHANNEL


async def main():
    # Create client with a session file for persistent login
    client = TelegramClient('signal_session', API_ID, API_HASH)
    # Authenticate with your phone number (interactive on first run)
    await client.start(phone=PHONE_NUMBER)
    print('Connected to Telegram!')


if __name__ == '__main__':
    asyncio.run(main())

What does this code do?

  • TelegramClient('signal_session', API_ID, API_HASH) creates a client that stores its login state in a file called signal_session.session. After the first login, this file lets you skip authentication on every future run.
  • client.start(phone=PHONE_NUMBER) connects and logs in. On the very first run, it triggers an interactive prompt in your terminal asking for a verification code.
  • asyncio.run(main()) starts the async event loop that Telethon needs to communicate with Telegram's servers.

What will Telethon prompt me for?

On the first run, Telethon sends a login code to your Telegram app and prompts you in the terminal with: Please enter the code you received:. Type the code and press Enter.

If you have two-factor authentication enabled, you will also see: Please enter your password:. Enter your 2FA password. After that, the session file is created and you will not be prompted again.

  • Save bot.py.
  • Run the script to test the connection by running:
python bot.py
  • When prompted, open your Telegram app, find the code Telegram sent you, and type it into the terminal.
  • You should see Connected to Telegram! printed in your terminal. The script then exits because there is nothing else for it to do yet.

Not seeing the code prompt?

  • Make sure PHONE_NUMBER in config.py includes the country code (e.g. +234...). Telethon cannot find your account without it.
  • Check that API_ID is an integer (no quotes) and API_HASH is a string (with quotes) in config.py.
  • If you see a flood wait error, wait the indicated time and try again. Telegram rate-limits login attempts.

Still stuck? Help me troubleshoot my Telethon connection.

Resolve the channel and register a message handler

The connection works. Now you need to tell the client which channel to watch and what to do when a new message arrives.

  • In bot.py, find the line print('Connected to Telegram!') and add the following code below it (still inside the main() function, above the if __name__ block):
    # Resolve channel from the invite link in config
    channel_entity = await client.get_entity(CHANNEL)
    print(f'Monitoring channel: {channel_entity.title}')

    # Fire this function every time a new message arrives
    @client.on(events.NewMessage(chats=channel_entity))
    async def signal_handler(event):
        message_text = event.raw_text
        print(f'[New Message] {message_text[:100]}')

    print('Listening for messages...')
    # Keep running until you press Ctrl+C
    await client.run_until_disconnected()

What does this code do?

  • client.get_entity(CHANNEL) takes your invite link and resolves it to a channel object. This works because you already joined the channel. No admin access needed.
  • @client.on(events.NewMessage(chats=channel_entity)) registers a callback that fires every time a new message appears in that specific channel.
  • event.raw_text gives you the plain text of the message without any formatting markup.
  • client.run_until_disconnected() keeps the script alive and listening. It blocks until you press Ctrl+C to stop.
  • Save bot.py.

The script will take over your terminal

Because run_until_disconnected() keeps the client running indefinitely, your terminal will appear to "hang" while it waits for messages. This is expected. Press Ctrl+C when you want to stop the bot.

Run the bot and verify live messages

Everything is wired up. Time to prove the listener works end-to-end.

  • Before you run the script, predict: what do you expect to see in the terminal after the bot connects?
  • Run the bot by running:
python bot.py
  • You should see three lines print immediately:
Connected to Telegram!
Monitoring channel: <your channel name>
Listening for messages...
  • Now wait for a message to appear in the signal channel. If no signal arrives soon, send a test message in another channel you own (and update CHANNEL in config.py temporarily) to confirm the handler fires.
  • When a message arrives, you should see it printed like:
[New Message] buy xauusd 7896, tp1 8000, tp2 8100, sl 7880
  • Press Ctrl+C to stop the bot when you are done testing.

Not seeing any messages?

  • Make sure CHANNEL in config.py matches the exact invite link you used to join. If the channel has a public username, you can also try "@channelname" instead.
  • Confirm you are still a member of the channel in your Telegram app. If you left, the handler cannot receive messages.
  • If you see a "Could not find the input entity" error, the invite link may have expired. Rejoin the channel and copy a fresh invite link.

Still stuck? Help me debug why my Telegram channel listener is not receiving messages.

✔️ Awesome, I've got everything!

Great. Double check you have saved bot.py and that the script printed live messages from your channel.

ⓧ I'd like to double check the full code

Here is the complete bot.py at the end of this step:

import asyncio
from telethon import TelegramClient, events
from config import API_ID, API_HASH, PHONE_NUMBER, CHANNEL

async def main():
    client = TelegramClient('signal_session', API_ID, API_HASH)
    await client.start(phone=PHONE_NUMBER)
    print('Connected to Telegram!')
    channel_entity = await client.get_entity(CHANNEL)
    print(f'Monitoring channel: {channel_entity.title}')

    @client.on(events.NewMessage(chats=channel_entity))
    async def signal_handler(event):
        message_text = event.raw_text
        print(f'[New Message] {message_text[:100]}')

    print('Listening for messages...')
    await client.run_until_disconnected()

if __name__ == '__main__':
    asyncio.run(main())

Your bot can now hear every message that drops in the signal channel. Next up, you will build a parser that separates real trading signals from noise and extracts the exact trade parameters.

Parse Trading Signals from Raw Messages

Your bot can now read messages from the Telegram channel in real-time. The problem is that every message looks the same to it. A trading signal like "buy xauusd 7896" and a casual message like "good morning everyone" both arrive as plain text with no structure.

In this step, you will build a signal parser that examines each message and extracts the trade parameters: direction, symbol, entry price, stop-loss, and take-profit. Messages that do not match the signal pattern get ignored.

In this step, get ready to:
  • Write a regex-based parse_signal() function that extracts trade parameters from raw text.
  • Handle edge cases like non-signal messages, mixed casing, extra commas, and multiple take-profit levels.
  • Test the parser with sample signals and verify it extracts all fields correctly.
Create the signal parser and extract direction and symbol

Trading signals follow a predictable pattern: a direction (buy or sell), a symbol (like XAUUSD or EURUSD), an entry price, take-profit levels, and a stop-loss. The challenge is that different signal providers format these differently. Some use uppercase, some lowercase. Some separate values with commas, others with spaces.

Regex (regular expressions) lets you define flexible patterns that match all these variations at once.

  • Create a new file called parser.py in your project folder and add the following code:
import re


def parse_signal(message_text):
    """
    Parse a trading signal from raw message text.

    Expected formats:
    - buy xauusd 7896, tp1 8000, tp2 8100, sl 7880
    - SELL EURUSD 1.0850 TP 1.0800 SL 1.0900

    Returns a dict with direction, symbol, entry, sl, tp or None if not a signal.
    """
    if not message_text:
        return None

    text = message_text.strip().lower()

    direction_match = re.match(r"(buy|sell)\s+", text)
    if not direction_match:
        return None

    direction = direction_match.group(1)

    symbol_match = re.search(r"(buy|sell)\s+([a-z]{6,10})\s+", text)
    if not symbol_match:
        return None

    symbol = symbol_match.group(2).upper()

What does this code do?

  • text = message_text.strip().lower() normalises the message to lowercase so "BUY" and "buy" both match the same pattern.
  • re.match(r"(buy|sell)\s+", text) checks if the message starts with "buy" or "sell". If not, it is not a signal and the function returns None immediately.
  • The symbol regex [a-z]{6,10} matches 6-10 letter currency pairs like xauusd, eurusd, or gbpjpy. The result is converted to uppercase for MT5 compatibility.
Extract entry price, stop-loss, and take-profit levels

The second half of the parser pulls out the numeric values. Signals can have multiple take-profit levels (tp1, tp2, tp3), so the parser collects all of them into a list and uses the first one as the primary TP for the trade.

  • In parser.py, add the following code directly below the symbol = symbol_match.group(2).upper() line (still inside the function):
    entry_match = re.search(r"(buy|sell)\s+[a-z]{6,10}\s+([\d.]+)", text)
    if not entry_match:
        return None

    entry = float(entry_match.group(2))

    sl_match = re.search(r"sl\s*([\d.]+)", text)
    if not sl_match:
        return None

    sl = float(sl_match.group(1))

    tp_matches = re.findall(r"tp\d?\s*([\d.]+)", text)
    if not tp_matches:
        return None

    tps = [float(tp) for tp in tp_matches]

    return {
        "direction": direction,
        "symbol": symbol,
        "entry": entry,
        "sl": sl,
        "tp": tps[0],
        "tps": tps,
    }

What does this code do?

  • The entry price regex grabs the number that appears right after the symbol (e.g. 7896 from "buy xauusd 7896").
  • sl\s*([\d.]+) finds "sl" followed by optional whitespace and then a number. The \s* handles signals with or without a space after "sl".
  • re.findall(r"tp\d?\s*([\d.]+)", text) collects ALL take-profit levels at once. The \d? makes the number after "tp" optional, so it matches both tp1 8000 and tp 8000.
  • The return dictionary uses tps[0] as the primary TP (the first level) while storing the full list in tps for future use.
  • Save parser.py.
  • Verify the function works by running this quick test in your terminal:
python -c "from parser import parse_signal; print(parse_signal('buy xauusd 7896 tp1 8000 sl 7880'))"

You should see a dictionary printed with direction, symbol, entry, sl, and tp fields populated.

Seeing a SyntaxError or None?

  • If you see a SyntaxError, check that the indentation of the code you just added lines up with the rest of the function body (4 spaces).
  • If the output is None, make sure the return statement and the tps list are inside the function, not at the top level.
  • Make sure you are running the command from your project folder where parser.py lives.

Help me debug my parse_signal function

Test the parser with sample signals

A single test proves the function works for one case. To build confidence that it handles real-world variations, you need to test with multiple formats and non-signal messages.

  • Add the following test block at the bottom of parser.py, outside and below the parse_signal() function (no indentation):
if __name__ == "__main__":
    test_signals = [
        "buy xauusd 7896,,, tp1 8000, tp2 8100, sl 7880",
        "SELL EURUSD 1.0850 TP1 1.0800 TP2 1.0750 SL 1.0900",
        "Hello everyone, good morning!",
        "buy gbpjpy 198.50 tp 199.00 sl 198.00",
    ]

    for signal_text in test_signals:
        result = parse_signal(signal_text)
        print(f"Input: {signal_text}")
        print(f"Output: {result}")
        print()

What does this test block do?

  • if __name__ == "__main__": means this code only runs when you execute parser.py directly. It will not run when other files import the parse_signal function.
  • The test list includes: a signal with extra commas, an all-uppercase signal, a non-signal message ("Hello everyone"), and a clean signal with decimals. This covers the realistic variations you will see in a live channel.
  • Save parser.py.

Before you run this, think about what you expect to see for the third test message ("Hello everyone, good morning!"). Should it return a dictionary or something else?

  • Run the parser tests by running this command:
python parser.py

You should see output for all four test cases. The first, second, and fourth messages produce dictionaries with the extracted trade parameters. The third message ("Hello everyone, good morning!") outputs None because it does not match the signal pattern.

What should I see?

For the first test signal buy xauusd 7896,,, tp1 8000, tp2 8100, sl 7880, the output should show:

  • direction: buy
  • symbol: XAUUSD
  • entry: 7896.0
  • sl: 7880.0
  • tp: 8000.0 (the first take-profit level)

Notice that the extra commas in the input did not break the parser. The regex patterns skip over punctuation and whitespace variations gracefully.

Seeing None for every signal?

  • Check that your regex strings use a raw string prefix (r"..."). Without the r, backslashes get interpreted as escape characters instead of regex patterns.
  • Make sure the text = message_text.strip().lower() line is present near the top of the function. Without it, uppercase signals will not match the lowercase regex patterns.
  • Verify that each if not ..._match: return None block is properly indented inside the function.

Help me figure out why my parser returns None for valid signals

✔️ Awesome, I've got everything!

Great. Make sure you have saved parser.py and that all four test cases produce the expected output.

ⓧ I'd like to double check the full code

Scroll up and confirm these three code blocks are present in your parser.py, in this order:

  • The import re line, the function definition with docstring, the direction and symbol extraction (ending at symbol = symbol_match.group(2).upper()).
  • The entry, SL, and TP extraction (starting at entry_match and ending at the return dictionary).
  • The if __name__ == "__main__": test block at the bottom with four sample signals.

Your parser can now turn unstructured text into structured trade data. Next up, you will connect to your MetaTrader 5 terminal and prove you can execute a trade programmatically.

Connect to MT5 and Execute a Test Trade

Your signal parser can extract trade parameters from raw text. But parsed data sitting in a dictionary is useless unless it reaches your trading platform.

In this step, you will connect Python directly to your MetaTrader 5 terminal and prove you can open a real position programmatically. By the end, a single function call turns a signal dictionary into a live trade on your AquaFunded account.

In this step, get ready to:
  • Write a connection function that initializes MT5 and verifies login to your AquaFunded account.
  • Build an execute_trade() function that constructs and sends a market order with the correct SL and TP.
  • Execute a test trade and confirm it appears in your MT5 terminal.
Write the MT5 connection code

Before you can send any orders, your Python script needs to initialize the MT5 terminal process and authenticate with your broker credentials.

  • In VS Code, create a new file called trader.py inside your project folder.
  • Add the imports and the connect_mt5() function by pasting this code:
import MetaTrader5 as mt5
from config import MT5_LOGIN, MT5_PASSWORD, MT5_SERVER, LOT_SIZE, MAGIC_NUMBER, DEVIATION


def connect_mt5():
    """Initialize and connect to the MT5 terminal."""
    if not mt5.initialize():
        print(f"MT5 initialize failed, error: {mt5.last_error()}")
        return False

    if MT5_LOGIN and MT5_PASSWORD and MT5_SERVER:
        authorized = mt5.login(MT5_LOGIN, password=MT5_PASSWORD, server=MT5_SERVER)
        if not authorized:
            print(f"MT5 login failed, error: {mt5.last_error()}")
            mt5.shutdown()
            return False

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

    print(f"Connected to MT5: {account_info.server}")
    print(f"Account: {account_info.login}, Balance: {account_info.balance}")
    return True

What does this code do?

This function handles the full MT5 startup sequence in three stages:

  • mt5.initialize() launches the MT5 terminal process so Python can communicate with it.
  • mt5.login() authenticates with your AquaFunded broker using the credentials from config.py.
  • mt5.account_info() confirms the connection succeeded by fetching your account details. If any stage fails, the function prints the error and shuts down cleanly.
  • Below connect_mt5(), add the test runner block that calls the function when you run the file directly:
if __name__ == "__main__":
    if connect_mt5():
        account = mt5.account_info()
        print(f"\nAccount equity: {account.equity}")
        print(f"Free margin: {account.margin_free}")
        mt5.shutdown()

What does this block do?

The if __name__ == "__main__" guard means this code only runs when you execute trader.py directly. It connects, prints your equity and free margin, then shuts down the MT5 link.

  • Save trader.py.
  • Make sure your MT5 terminal is open and logged in to your AquaFunded account.
  • Run the connection test by running this command:
python trader.py

You should see output showing your server name, account login number, balance, equity, and free margin.

Seeing 'MT5 initialize failed'?

  • Make sure the MetaTrader 5 terminal application is open and fully loaded (not minimized to tray) before running the script.
  • Check that your MT5_LOGIN, MT5_PASSWORD, and MT5_SERVER values in config.py match exactly what your AquaFunded account uses. The server name is case-sensitive.
  • If you see a login failure, verify your credentials by logging in manually through MT5 first.

Help me debug my MT5 connection error

Build the execute_trade() function

Now that you have a confirmed connection, you need a function that takes a parsed signal dictionary and turns it into a live market order.

  • In trader.py, find the line that says if __name__ == "__main__":. Add the first part of execute_trade() above it, with two blank lines separating them:
def execute_trade(signal):
    """Execute a trade based on a parsed signal dict."""
    symbol = signal["symbol"]
    direction = signal["direction"]
    sl = signal["sl"]
    tp = signal["tp"]

    symbol_info = mt5.symbol_info(symbol)
    if symbol_info is None:
        print(f"Symbol {symbol} not found")
        return None

    if not symbol_info.visible:
        if not mt5.symbol_select(symbol, True):
            print(f"Failed to select symbol {symbol}")
            return None

    if direction == "buy":
        order_type = mt5.ORDER_TYPE_BUY
        price = mt5.symbol_info_tick(symbol).ask
    else:
        order_type = mt5.ORDER_TYPE_SELL
        price = mt5.symbol_info_tick(symbol).bid

What does this code do?

  • mt5.symbol_info() checks that the symbol (e.g. XAUUSD) exists on your broker. If your broker uses a suffix like "XAUUSDm", this check catches it early.
  • mt5.symbol_select() makes the symbol visible in Market Watch, which is required before you can trade it.
  • The price is fetched live from the current tick. Buy orders use the ask price. Sell orders use the bid price.
  • Directly below the code you just added (still inside execute_trade()), add the order request and result handling:
    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": "signal_copier",
        "type_time": mt5.ORDER_TIME_GTC,
        "type_filling": mt5.ORDER_FILLING_IOC,
    }

    result = mt5.order_send(request)

    if result is None:
        print(f"order_send failed, no result returned")
        return None

    if result.retcode != mt5.TRADE_RETCODE_DONE:
        print(f"Trade failed: retcode={result.retcode}, comment={result.comment}")
        return None

    print(f"Trade executed: {direction.upper()} {symbol} @ {result.price}")
    print(f"  SL: {sl}, TP: {tp}, Ticket: {result.order}")
    return result

What does this request do?

  • mt5.TRADE_ACTION_DEAL tells MT5 this is an immediate market execution, not a pending order.
  • LOT_SIZE comes from your config (0.01). DEVIATION is the maximum allowed slippage in points.
  • MAGIC_NUMBER tags the trade so you can identify bot-placed orders later.
  • ORDER_FILLING_IOC (Immediate or Cancel) is the filling mode supported by most CFD prop firm brokers like AquaFunded.
  • After sending, the function checks result.retcode against TRADE_RETCODE_DONE to confirm the order was filled.
  • Save trader.py.
Execute a test trade on your account

Everything is wired up. Time to prove it works by sending a real order to your AquaFunded account with a minimal lot size.

Before you run this, do you think the trade will succeed or fail? Consider whether the market is open for the symbol you are testing.

  • Execute a test trade by running this command in your terminal:
python -c "from trader import connect_mt5, execute_trade; connect_mt5(); execute_trade({'direction': 'buy', 'symbol': 'XAUUSD', 'entry': 2500.0, 'sl': 2490.0, 'tp': 2520.0})"

What does this command do?

This imports your functions, connects to MT5, and calls execute_trade() with a test signal dictionary. It uses XAUUSD (gold) with a 0.01 lot size, a stop-loss 10 points below entry, and a take-profit 20 points above.

If the market is closed (weekends or holidays), you will see a trade failure with a retcode indicating the market is off. That is expected. Try again when the market is open, or test with a symbol that trades 24/7 on your broker.

If the trade succeeds, you will see output like:

Connected to MT5: YourServer
Account: 12345678, Balance: 100000.0
Trade executed: BUY XAUUSD @ 2500.12
  SL: 2490.0, TP: 2520.0, Ticket: 987654321
  • Switch to your MT5 terminal and check the Trade tab at the bottom. You should see a new open position with the correct symbol, direction, SL, and TP values.
  • Close the test position manually in MT5 by right-clicking it and selecting Close Position.

Trade failed with a retcode error?

  • Retcode 10018 (Market is closed): wait until the market opens for your chosen symbol. Forex pairs trade Sunday evening to Friday evening. Gold (XAUUSD) follows a similar schedule.
  • Retcode 10030 (Invalid filling type): your broker may not support ORDER_FILLING_IOC. Try changing it to mt5.ORDER_FILLING_FOK in your request dictionary.
  • "Symbol not found": your broker may use a suffix like XAUUSDm or XAUUSD.. Check the symbol name in MT5's Market Watch panel and use that exact name in your test.

Help me fix my MT5 trade execution error

✔️ Awesome, I've got everything!

Great. Make sure you have saved trader.py and closed your test position in MT5.

ⓧ I'd like to double check the full code

Your trader.py file is built from the chunks above. Scroll up and confirm each section is present in this order:

  • Imports (import MetaTrader5 as mt5 and the config imports).
  • The connect_mt5() function (initialize, login, account_info check).
  • The execute_trade(signal) function (symbol check, price fetch, request dict, order_send, result handling).
  • The if __name__ == "__main__": block (connect, print equity and margin, shutdown).

You just proved that Python can open a live trade on your MT5 terminal with the correct parameters. Next up, you will wire the Telegram listener, signal parser, and trade executor together into a single bot that copies signals automatically.

Wire Everything Together into a Live Bot

Your Telegram listener can read messages. Your parser can extract trade signals. Your trader can place orders on MT5. Right now, they are three separate scripts that do nothing together.

In this step, you will wire all three pieces into a single live bot. When a signal drops in the channel, the bot parses it and fires the trade automatically with no manual intervention.

In this step, get ready to:
  • Combine the Telegram event handler with the signal parser and trade executor in a single script.
  • Add error handling for MT5 rejections, non-signal messages, and connection failures.
  • Run the complete bot and watch it operate end-to-end.
Integrate the parser and trader into bot.py

The existing bot.py only prints messages to the console. You need to replace its content so it imports and calls your parser and trader modules.

  • Open bot.py in VS Code.
  • Select all the existing code and delete it.
  • Replace it with this code that wires up MT5 connection and Telegram authentication:
import asyncio
from telethon import TelegramClient, events
from config import API_ID, API_HASH, PHONE_NUMBER, CHANNEL
from parser import parse_signal
from trader import connect_mt5, execute_trade
import MetaTrader5 as mt5


async def main():
    print("Starting Signal Copier Bot...")

    if not connect_mt5():
        print("Failed to connect to MT5. Make sure the terminal is running.")
        return

    client = TelegramClient("signal_session", API_ID, API_HASH)
    await client.start(phone=PHONE_NUMBER)

    print("Connected to Telegram!")

    try:
        channel_entity = await client.get_entity(CHANNEL)
        print(f"Monitoring channel: {channel_entity.title}")
    except Exception as e:
        print(f"Failed to resolve channel: {e}")
        mt5.shutdown()
        return

What does this code do?

  • The new imports bring in parse_signal from your parser and connect_mt5 / execute_trade from your trader module.
  • MT5 connects first. If it fails (terminal not running, wrong credentials), the bot exits immediately instead of listening to signals it cannot act on.
  • The channel resolution is wrapped in a try/except so a bad invite link or network issue gives a clear error message instead of crashing the whole script.
  • Directly below the code you just pasted, add the signal handler and main execution block:
    @client.on(events.NewMessage(chats=channel_entity))
    async def signal_handler(event):
        message_text = event.raw_text
        print(f"\n[New Message] {message_text[:100]}")

        signal = parse_signal(message_text)

        if signal is None:
            print("  -> Not a valid signal, skipping.")
            return

        print(f"  -> Signal detected: {signal['direction'].upper()} {signal['symbol']}")
        print(f"     Entry: {signal['entry']}, SL: {signal['sl']}, TP: {signal['tp']}")

        result = execute_trade(signal)

        if result:
            print(f"  -> Trade placed successfully! Ticket: {result.order}")
        else:
            print(f"  -> Trade execution failed.")

    print("\nBot is running. Waiting for signals...")
    print("Press Ctrl+C to stop.\n")

    await client.run_until_disconnected()


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\nBot stopped.")
    finally:
        mt5.shutdown()

What does this code do?

  • The signal_handler now calls parse_signal() on every new message. If parsing returns None, the message is not a trade signal and the bot skips it.
  • When a valid signal is detected, it logs the parameters and calls execute_trade(). The result check handles MT5 rejections (invalid symbol, market closed, insufficient margin).
  • The KeyboardInterrupt handler lets you stop the bot cleanly with Ctrl+C. The finally block always shuts down MT5, even if the bot crashes.
  • Save bot.py.

Seeing red underlines on the imports?

If VS Code shows errors on from parser import parse_signal or from trader import connect_mt5, make sure parser.py and trader.py are in the same folder as bot.py. All four files (config.py, parser.py, trader.py, bot.py) must live in the same directory.

help me fix import errors in my signal copier bot

✔️ Awesome, I've got everything!

Great. Make sure you have saved bot.py before running it.

ⓧ I'd like to double check the full code

Your bot.py should contain these sections in order. Scroll up and confirm each is present:

  • Six import lines at the top (asyncio, TelegramClient, events, config values, parse_signal, connect_mt5, execute_trade, MetaTrader5).
  • The async def main(): function starting with the MT5 connection check, then Telegram client start, then the try/except channel resolution.
  • The signal_handler function decorated with @client.on(events.NewMessage(chats=channel_entity)), calling parse_signal and execute_trade.
  • The if __name__ == "__main__": block with try/except for KeyboardInterrupt and a finally that calls mt5.shutdown().
Run the bot end-to-end

Everything is connected. Before you run it, think about what you expect to see. The bot will try to connect to MT5, then to Telegram, then sit and wait for a channel message.

What do you think will happen when a non-signal message (like casual chat) arrives versus a properly formatted trade signal?

  • Make sure your MetaTrader 5 terminal is open and logged in.
  • Run the bot by executing this command in your terminal:
python bot.py

You should see output confirming both connections succeeded:

Starting Signal Copier Bot...
Connected to MT5: your-aquafunded-server
Account: 12345678, Balance: 100000.0
Connected to Telegram!
Monitoring channel: Your Signal Channel

Bot is running. Waiting for signals...
Press Ctrl+C to stop.

What should I see?

The bot is now actively monitoring the channel. When a message arrives, you will see one of two outcomes in the console:

  • Non-signal message: prints the text and "Not a valid signal, skipping."
  • Valid signal: prints the parsed parameters and either "Trade placed successfully!" or the reason it failed (market closed, invalid symbol).
  • Wait for a message to appear in the signal channel. If you want to test immediately, send a message in a different channel you own that you point the bot to.
  • When a valid signal arrives and the market is open, check your MT5 terminal to confirm a new position opened with the correct symbol, direction, stop-loss, and take-profit.
  • Press Ctrl+C to stop the bot when you are done testing.

Bot exits immediately with an MT5 error?

  • Make sure the MetaTrader 5 terminal is open and logged in before running the script. The bot cannot initialize MT5 if the terminal is closed.
  • Check that MT5_LOGIN, MT5_PASSWORD, and MT5_SERVER in config.py match your AquaFunded account credentials exactly.
  • If you see "Failed to resolve channel", double-check that the CHANNEL value in config.py is the correct invite link and that you are still a member of the channel on your Telegram account.

help me debug my signal copier bot startup

Market closed? That's OK

If the market is closed when a signal arrives, you will see a trade failure with a retcode indicating the market is off. The bot still detected and parsed the signal correctly. When the market reopens, the same flow will execute trades successfully.

Your signal copier bot is fully operational. It reads from Telegram, parses signals, and places trades on your AquaFunded account. Next up, a Secret Mission to make it trade smarter by sizing positions based on your account balance and risk tolerance.

Secret mission

Add Risk-Based Lot Sizing

Your bot trades a fixed 0.01 lots on every signal, regardless of your account size or how far away the stop-loss is. Professional traders size each position so they never risk more than 1-2% of their account on a single trade. In this mission, you'll build dynamic lot sizing that calculates the perfect position size for every signal.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally on your machine, so there are no ongoing cloud costs or subscriptions to worry about. Decide whether to keep your resources as they are, pause things to come back later, or delete them entirely.

Resources you used:

  • Project folder containing config.py, parser.py, trader.py, bot.py, and the signal_session.session file.
  • Python packages: telethon and MetaTrader5.
  • Any open test trades on your MT5 account from Step 4.

Keep everything running

No action needed. Choose this if you plan to keep running the bot or want to tweak the code further.

  • Your bot only runs when you manually start it with python bot.py. There are no background processes or recurring charges.
  • Your Telegram API app at my.telegram.org persists at no cost. It allows your bot to authenticate in future sessions.
  • Check your MT5 terminal for any open test trades from Step 4. Close them manually if you do not want them running on your account.

Pause - I'll come back to this later

Shut down the bot and close test positions so nothing runs while you are away. Your code and session file stay intact for next time.

  • Stop the bot by pressing Ctrl+C in the terminal if it is still running.
  • Open your MT5 terminal and close any open test trades from Step 4.
  • Your signal_session.session file keeps your Telegram login cached. You will not need to re-authenticate with your phone number next time you run the bot.

Delete - I don't want to use this again

Remove all project files, uninstall packages, and close any test trades so your machine is back to its original state.

  • Open your MT5 terminal and close any open test trades placed during Step 4.
  • Delete your project folder (the one containing config.py, parser.py, trader.py, and bot.py). Open your terminal and navigate to your Desktop, then run the appropriate command below.

macOS

rm -rf ~/Desktop/signal-copier

Windows

rmdir /s /q "$HOME\Desktop\signal-copier"
  • Uninstall the Python packages by running:
pip uninstall -y telethon MetaTrader5
  • Confirm the packages are removed by running:
pip list | grep -i "telethon\|metatrader"

If nothing is returned, both packages are fully removed.

Nice Work!

Nice Work!

You just built a fully automated trading signal copier from scratch. Your bot monitors a Telegram channel, parses raw signal text into structured trade parameters, and fires off live trades on your MetaTrader 5 terminal with zero manual intervention.

You've learned how to:

  • Build a Telethon-based userbot that reads any Telegram channel as a regular member with no admin access required.
  • Create a regex signal parser that extracts direction, symbol, entry, SL, and TP from real-world signal messages with inconsistent formatting.
  • Connect Python to MetaTrader 5 and execute trades programmatically with proper stop-loss and take-profit, wiring all components into a live-running bot that copies signals automatically.
  • Secret Mission: Added risk-based dynamic lot sizing so each trade risks a controlled percentage of your account balance.

Ready to quiz yourself?