MT5 Python API Guide: Data Retrieval, Order Validation, and Safer Automated Trading

Conclusion

The MT5 Python API is an interface for connecting a Python script to a MetaTrader 5 terminal, retrieving price data, checking account details and positions, validating orders, and sending orders.
For beginners, the easiest way to understand the overall workflow is to learn initialize(), last_error(), copy_rates_from(), positions_get(), order_check(), order_send(), and shutdown() in that order.
Keep analysis-only processing separate from order submission. Before sending orders in a live environment, test the workflow with a demo account and verify broker specifications, lot sizes, margin requirements, spreads, and filling policies.

1. What Is the MT5 Python API?

Conclusion: The MT5 Python API is an API for connecting Python to a MetaTrader 5 terminal and working with market data and trading information. It connects Python-based analysis with the MT5 trading environment.

With the MT5 Python API, Python can work with candlestick bars, ticks, account details, orders, positions, and trading history. The retrieved data can be used for aggregation, visualization, statistical analysis, and machine learning datasets.

A Python script does not connect directly to a trading server on its own. The basic flow is to connect the Python script to a MetaTrader 5 terminal and handle the required information through that terminal.

What You Can Do with the MT5 Python API

  • Connect to and disconnect from an MT5 terminal
  • Retrieve account and terminal information
  • Retrieve symbol information, Bid, and Ask prices
  • Retrieve candlestick bar and tick data
  • Check current orders and positions
  • Check margin requirements for an order
  • Send order requests
  • Retrieve order and deal history

2. What to Understand Before You Start

Conclusion: First, understand that the MT5 Python API is not the same as an MQL5 program. The Python API lets an external script operate the terminal. It does not directly use EA event functions.

An MQL5 EA usually runs with event functions such as OnInit(), OnTick(), and OnDeinit(). A Python script instead defines its own sequence for connection, data retrieval, decision-making, waiting, and shutdown.

ApproachAdvantagesDisadvantagesSuitable Use Cases
Python scriptEasy to use with analysis librariesContinuous monitoring and reconnection must be designed explicitlyData analysis, validation tools, and integration with external processing
MQL5 EAEasy to run in response to ticks and trading eventsPython analysis assets may be difficult to use directlyAutomated trading completed inside MT5 and low-latency monitoring
Separate Python and MQL5 responsibilitiesMakes analysis and execution responsibilities easier to separateRequires a design for integration and failure handlingSystems with complex analysis logic

Beginners should start by creating an analysis-only script. Adding order submission later makes it easier to distinguish connection failures from invalid order conditions.

3. Installation and Connection

Conclusion: Install the MetaTrader5 package in Python and connect to the MT5 terminal with initialize(). If the connection fails, check the details returned by last_error().

Install the package with the following command.

pip install MetaTrader5

Use the following command to update it.

pip install --upgrade MetaTrader5

The following is a minimal connection test.

import MetaTrader5 as mt5


def main() -> None:
    if not mt5.initialize():
        raise RuntimeError(f"initialize() failed: {mt5.last_error()}")

    try:
        print("terminal:", mt5.terminal_info())
        print("version:", mt5.version())
        print("account:", mt5.account_info())
    finally:
        mt5.shutdown()


if __name__ == "__main__":
    main()

initialize() can search for a terminal to connect to when called without arguments. If you use multiple MT5 terminals, specify the path to the target executable to make the connection destination easier to manage.

if not mt5.initialize(r"C:\Path\To\terminal64.exe"):
    raise RuntimeError(f"initialize() failed: {mt5.last_error()}")

Writing an account number, password, and server name directly in code increases the risk of credential leakage. If credentials are required, manage them with environment variables or a configuration file with restricted permissions.

4. Overview of Commonly Used Functions

Conclusion: The MT5 Python API is easier to understand when divided into five groups: connection, information retrieval, price data retrieval, trading management, and history retrieval. You do not need to memorize every function at the beginning.

CategoryMain FunctionsPurpose
Connectioninitialize(), login(), shutdown()Connect to the terminal, log in to an account, and disconnect
Errors and environmentlast_error(), terminal_info(), account_info()Check errors and terminal and account status
Symbolssymbols_get(), symbol_info(), symbol_info_tick(), symbol_select()Check symbol lists, specifications, Bid, Ask, and visibility
Price datacopy_rates_from(), copy_rates_range(), copy_ticks_from()Retrieve candlestick bars and ticks
Orders and positionsorders_get(), positions_get(), order_check(), order_send()Check orders and positions, validate orders, and send orders
Historyhistory_orders_get(), history_deals_get()Retrieve order and deal history
MT5 Python API execution flow from initialize() and rate retrieval to order_check(), order_send(), last_error(), and shutdown().

An analysis-only script may only need to connect, verify the symbol, retrieve price data, and disconnect. Put order submission in a separate module to reduce the risk of unintended execution.

5. How to Retrieve Candlestick Data

Conclusion: Use copy_rates_from() to retrieve candlestick data. Confirm that the return value is not None and that the required number of records was retrieved.

copy_rates_from() retrieves bars for a specified symbol, timeframe, start time, and number of records. The result includes time, open, high, low, close, tick volume, spread, and real volume.

from datetime import datetime, timezone

import MetaTrader5 as mt5
import pandas as pd


def get_rates(symbol: str, count: int = 300) -> pd.DataFrame:
    start = datetime(2025, 1, 1, tzinfo=timezone.utc)
    rates = mt5.copy_rates_from(symbol, mt5.TIMEFRAME_H1, start, count)

    if rates is None:
        raise RuntimeError(f"copy_rates_from() failed: {mt5.last_error()}")

    if len(rates) < count:
        print(f"warning: requested={count}, received={len(rates)}")

    frame = pd.DataFrame(rates)
    frame["time"] = pd.to_datetime(frame["time"], unit="s", utc=True)
    return frame


if not mt5.initialize():
    raise RuntimeError(f"initialize() failed: {mt5.last_error()}")

try:
    print(get_rates("EURUSD").tail())
finally:
    mt5.shutdown()

MT5 bar and tick times are handled as UTC. When creating a Python datetime, specify UTC to reduce the risk of retrieving an incorrect time range.

If the required amount of historical data cannot be retrieved, also check the historical range loaded in the MT5 terminal. Ignoring missing records can make calculations such as moving averages and feature values unstable.

6. How to Check Symbol Information and Current Prices

Conclusion: Before analyzing or submitting an order, use symbol_info() and symbol_info_tick() to check symbol specifications and current prices. If the symbol is hidden, symbol_select() may be able to add it to Market Watch.

import MetaTrader5 as mt5


def get_symbol_snapshot(symbol: str):
    info = mt5.symbol_info(symbol)
    if info is None:
        raise RuntimeError(f"symbol_info() failed: {symbol}, {mt5.last_error()}")

    if not info.visible and not mt5.symbol_select(symbol, True):
        raise RuntimeError(f"symbol_select() failed: {symbol}, {mt5.last_error()}")

    tick = mt5.symbol_info_tick(symbol)
    if tick is None:
        raise RuntimeError(f"symbol_info_tick() failed: {symbol}, {mt5.last_error()}")

    return info, tick

Symbol names may differ between brokers. For example, in an environment that uses suffixes, the fixed string EURUSD may not work as-is.

7. How to Check Positions and Orders

Conclusion: Use positions_get() for open positions and orders_get() for pending orders. A None result may indicate a retrieval error and must be handled separately from an empty result.

import MetaTrader5 as mt5


def show_positions(symbol: str) -> None:
    positions = mt5.positions_get(symbol=symbol)

    if positions is None:
        raise RuntimeError(f"positions_get() failed: {mt5.last_error()}")

    if not positions:
        print(f"no open positions: {symbol}")
        return

    for position in positions:
        print(
            position.ticket,
            position.symbol,
            position.volume,
            position.price_open,
            position.profit,
        )

MT5 handles positions differently for netting and hedging accounts. With a netting account, positions for the same symbol are consolidated. With a hedging account, multiple positions can exist for the same symbol, so manage the ticket of the position to close explicitly.

8. Order Validation and Submission

Conclusion: Before calling order_send(), run order_check(). A successful order_check() does not guarantee execution, so always check the returned retcode after submission.

At a minimum, validate the following conditions before submitting an order.

  • Automated trading is permitted
  • The symbol is tradable
  • The symbol name matches the environment
  • The volume matches the minimum lot, maximum lot, and lot step
  • Margin is sufficient
  • The spread is within the acceptable range
  • Stop-loss and take-profit levels do not violate the stop level
  • The filling policy and type_filling match the symbol specifications
  • Existing positions and the account type have been considered

The following validation sample creates an order request and calls order_check(). It does not call order_send(), so this code alone does not submit an order.

import MetaTrader5 as mt5


def validate_buy_request(symbol: str, volume: float):
    info = mt5.symbol_info(symbol)
    if info is None:
        raise RuntimeError(f"symbol_info() failed: {symbol}, {mt5.last_error()}")

    if not info.visible and not mt5.symbol_select(symbol, True):
        raise RuntimeError(f"symbol_select() failed: {symbol}, {mt5.last_error()}")

    tick = mt5.symbol_info_tick(symbol)
    if tick is None:
        raise RuntimeError(f"symbol_info_tick() failed: {symbol}, {mt5.last_error()}")

    if volume < info.volume_min or volume > info.volume_max:
        raise ValueError("volume is outside symbol limits")

    steps = round((volume - info.volume_min) / info.volume_step)
    normalized_volume = info.volume_min + steps * info.volume_step
    if abs(normalized_volume - volume) > 1e-9:
        raise ValueError("volume does not match volume_step")

    request = {
        "action": mt5.TRADE_ACTION_DEAL,
        "symbol": symbol,
        "volume": volume,
        "type": mt5.ORDER_TYPE_BUY,
        "price": tick.ask,
        "deviation": 10,
        "magic": 20250601,
        "comment": "python validation sample",
        "type_time": mt5.ORDER_TIME_GTC,
        "type_filling": mt5.ORDER_FILLING_RETURN,
    }

    check = mt5.order_check(request)
    if check is None:
        raise RuntimeError(f"order_check() failed: {mt5.last_error()}")

    return request, check

For actual submission, pass only validated requests to order_send(), then record the returned value and retcode.

def send_checked_request(request: dict):
    check = mt5.order_check(request)
    if check is None:
        raise RuntimeError(f"order_check() failed: {mt5.last_error()}")

    if check.retcode != 0:
        raise RuntimeError(f"order_check() rejected: {check}")

    result = mt5.order_send(request)
    if result is None:
        raise RuntimeError(f"order_send() failed: {mt5.last_error()}")

    if result.retcode != mt5.TRADE_RETCODE_DONE:
        raise RuntimeError(f"order_send() rejected: {result}")

    return result

This submission function is a minimal validation example. In a live system, add symbol-specific filling policies, a spread limit, stop-loss and take-profit levels, trading hours, existing-position checks, log storage, and resubmission conditions. Adjust type_filling to match the symbol specifications.

9. How to Think About Error Handling

Conclusion: In the MT5 Python API, check library-function failures separately from rejected orders. When a function returns None or False, inspect last_error(). After submitting an order, inspect the result object’s retcode.

A common troubleshooting sequence is as follows.

  1. Confirm that initialize() succeeded
  2. Display terminal_info() and account_info()
  3. Confirm the symbol name with symbol_info()
  4. Display the symbol with symbol_select()
  5. Confirm that symbol_info_tick() returns a value
  6. Inspect the return value from order_check()
  7. Inspect the return value and retcode from order_send()
  8. Save the input values and error details to a log when a failure occurs

Use last_error() to inspect library-function failures. After order_send() returns a result object, inspect the retcode to confirm the trading result.

10. Safer Design Patterns

Conclusion: To improve safety, separate analysis, decision-making, risk checks, order validation, order submission, position management, and logging. When all processing is packed into one function, troubleshooting failures becomes difficult.

The following structure is recommended.

ModulePurposeMain Checks
Connection managementConnect to, reconnect to, and disconnect from MT5Connection destination, timeouts, and shutdown handling
Data retrievalRetrieve bars, ticks, and symbol specificationsUTC, missing data, and record counts
Signal decisionCreate candidates from analysis resultsClosed bars and overfitting
Risk checksCheck lot sizes, stop-loss distance, and acceptable lossMinimum lot, maximum lot, and lot step
Order validationValidate requestsMargin, spread, and stop level
Order submissionSend only validated requestsretcode, resubmission conditions, and logs
Position managementManage open positions and closing targetsNetting accounts, hedging accounts, and tickets

Use configuration to separate analysis-only mode from order-enabled mode. In live operations, keeping order submission disabled by default makes the system easier to manage.

11. Backtesting and Forward Testing

Conclusion: A Python trading strategy requires not only data-based validation but also forward testing with a demo account. Backtest results do not guarantee future profits.

Items to Check in a Backtest

  • Total profit and loss
  • Maximum drawdown
  • Win rate
  • Profit factor
  • Number of trades
  • Consecutive losses
  • Spread assumptions
  • Period dependency
  • Parameter dependency
  • Missing data and time misalignment

Items to Check in a Forward Test

  • Differences from backtest results
  • Behavior when spreads widen
  • Execution delay and slippage
  • Handling of rejected orders
  • Trading frequency
  • Drawdown
  • Differences in broker conditions
  • Differences in execution conditions between demo and live accounts
  • Stability on a VPS or other continuous execution environment
  • Reconnection after disconnection and prevention of duplicate orders

Higher leverage can increase both profit and loss and can increase drawdown for the same price movement. Validate lot-size calculations and stop conditions separately from trading signals.

12. Common Failures

Conclusion: Common issues for beginners include overlooked connection destinations, symbol names, times, retrieved record counts, lot sizes, filling policies, and account types. Do not handle every error as one case. Check each processing stage separately.

Cannot Connect to MT5

Check the return value from initialize() and inspect last_error(). If multiple terminals exist, specify the path to the target executable.

Cannot Retrieve Candlestick Data

Check the symbol name, timeframe, UTC handling, and the historical range loaded in the terminal. Handle None separately from an insufficient number of retrieved records.

Order Validation Is Rejected

Check the lot size, margin, stop level, trading hours, symbol, and filling policy. Available conditions differ by broker specification.

An Order Is Not Executed as Expected After Submission

Do not judge success only by whether order_send() returned. Save the result’s retcode, execution price, and comment. Spreads, slippage, and execution delays can change the result.

The Wrong Position Is Closed

Confirm the difference between netting and hedging accounts, and specify a position ticket when required. Managing positions only by symbol can target the wrong position in an environment with multiple positions.

13. Live Trading Considerations

Conclusion: When using the MT5 Python API for automated trading, a successful connection does not mean the system is ready for stable operation. Before live use, conduct forward testing with a demo account and define shutdown and recovery conditions.

Consider the following risks in live operations.

  • Performance may deteriorate when spreads widen
  • Execution delays and slippage may occur
  • Behavior may change depending on broker symbol specifications
  • Execution conditions may differ between demo and live accounts
  • Disconnection and reconnection may cause duplicate orders
  • Strategies with strong parameter dependency tend to break down during forward testing
  • Overfitting may reduce reproducibility

Save order IDs, position tickets, request details, responses, errors, and timestamps in logs. Design the system so that it can restore its state after a shutdown, making incident investigation easier.

14. Summary

Conclusion: The MT5 Python API is easier to learn in the following order: connect, retrieve data, check state, validate orders, send orders, and shut down. Start with an analysis-only script, then add order handling step by step with a demo account.

The key points are to distinguish between last_error() and retcode, use UTC consistently, run order_check() before submitting an order, and consider differences between account types and broker specifications.

Related function references:

FAQ

What is the MT5 Python API?

The MT5 Python API is an interface for connecting a Python script to a MetaTrader 5 terminal and working with price data, account information, orders, positions, and history.

What do I need to install to use the MT5 Python API?

Install the MetaTrader5 package in your Python environment. You can also use pandas when you need to handle data in a tabular format.

What should I check if initialize() fails?

Check the details returned by last_error(), the MT5 terminal status, the target executable, and the account settings. If multiple terminals are installed, specify the target path to simplify troubleshooting.

Which function retrieves candlestick data?

Use copy_rates_from() when you want to specify a start time and a number of records. Confirm that the result is not None and that all required records were retrieved.

What should I know about time handling in the MT5 Python API?

Bar and tick times are handled as UTC. Using timezone-aware datetime values in Python helps avoid retrieving the wrong time range.

Does a successful order_check() guarantee execution?

No. order_check() validates the order, but it does not guarantee execution. After submitting an order, check the result from order_send() and its retcode.

What is the difference between netting and hedging accounts?

With a netting account, positions for the same symbol are consolidated. With a hedging account, multiple positions can exist for the same symbol, so managing the ticket of the position to close is important.

What should I check before starting automated trading with the MT5 Python API?

Start in analysis-only mode, then conduct forward testing with a demo account. Check spreads, execution differences, lot-size limits, margin, reconnection behavior, and duplicate-order prevention.