DEV Community

Cover image for Automated Trading with MetaTrader5: Order Management and Market Data Collection
Henrique Vital
Henrique Vital

Posted on

Automated Trading with MetaTrader5: Order Management and Market Data Collection

Your AsimovMT class provides a comprehensive interface for interacting with MetaTrader5 (MT5) using Python. However, there are several areas in your code that could benefit from improvements, corrections, and enhancements to ensure robustness and maintainability. Below is a detailed review with suggestions:


1. Initialization and Error Handling

Issue:

In the __init__ method, if mt5.initialize() fails, you print an error message and call mt5.shutdown(), but the program continues to execute. This can lead to unexpected behavior since subsequent operations depend on a successful initialization.

Recommendation:

After shutting down MT5, you should raise an exception or exit the program to prevent further execution.

Code Correction:

def __init__(self):
    if not mt5.initialize():
        print("initialize() failed, error code =", mt5.last_error())
        mt5.shutdown()
        raise ConnectionError("Failed to initialize MetaTrader5.")

    # Rest of your initialization code...
Enter fullscreen mode Exit fullscreen mode

2. Typographical Errors

Issue:

There is a consistent typo in the attribute name self.positons. It should be self.positions.

Impact:

This typo will lead to AttributeError when accessing or modifying self.positions elsewhere in the class.

Code Correction:

# Corrected attribute name
self.positions = [i._asdict() for i in mt5.positions_get()]
Enter fullscreen mode Exit fullscreen mode

Ensure that all instances of self.positons are corrected to self.positions throughout the class, including methods like check_positions_and_orders.


3. Use of self in the Main Block

Issue:

In the __main__ block, you use self to refer to the instance of AsimovMT. Typically, self is reserved for instance methods within a class.

Recommendation:

Use a different variable name (e.g., asimov_mt) to avoid confusion.

Code Correction:

if __name__ == "__main__":
    # Instance
    asimov_mt = AsimovMT()

    # Testing methods using asimov_mt instead of self
    start_date = datetime.today() - timedelta(days=1)
    df_data = asimov_mt.get_ohlc_range('PETR4', 'M1', start_date, datetime.today())
    df_data = asimov_mt.get_ohlc_pos('PETR4', 'M1', 0, 1000)

    # ... and so on
Enter fullscreen mode Exit fullscreen mode

4. Market Order Price Adjustment

Issue:

In the send_market_order method, you adjust the price by adding or subtracting 5 * ticks for buy and sell orders, respectively. Market orders are typically executed at the current market price without such adjustments.

Recommendation:

Use the current ask price for buy orders and bid price for sell orders without manual adjustments. If you intend to place a limit order, ensure that it aligns with your strategy.

Code Correction:

def send_market_order(self, symbol, side, volume, deviation=20, magic=1000, comment='test'):
    mt5.symbol_select(symbol, True)
    symbol_info = mt5.symbol_info(symbol)

    if side.lower() == 'buy':
        price = symbol_info.ask
        order_type = mt5.ORDER_TYPE_BUY
    elif side.lower() == 'sell':
        price = symbol_info.bid
        order_type = mt5.ORDER_TYPE_SELL
    else:
        raise ValueError("Invalid side. Use 'buy' or 'sell'.")

    if symbol_info.visible:
        request = {
            "action": mt5.TRADE_ACTION_DEAL,
            "symbol": symbol,
            "volume": volume,
            "type": order_type,
            "price": price,
            "deviation": deviation,
            "magic": magic,
            "comment": comment,
            "type_time": mt5.ORDER_TIME_GTC,
            "type_filling": mt5.ORDER_FILLING_RETURN,
        }
        result = mt5.order_send(request)
        if result.retcode != mt5.TRADE_RETCODE_DONE:
            print(f"Order failed, retcode={result.retcode}")
        return result
    else:
        print(f"{symbol} not visible. Unable to send market order.")
Enter fullscreen mode Exit fullscreen mode

5. Consistent Error Handling and Logging

Issue:

The current implementation uses print statements for error messages and notifications. This approach can be limiting for larger applications where logging levels and outputs need to be managed more granularly.

Recommendation:

Use Python’s built-in logging module to provide flexible and configurable logging.

Code Enhancement:

import logging

# Configure logging at the beginning of your script
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

# Replace print statements with logging
logging.info("MetaTrader5 loaded. Ready to start.")
logging.error(f"initialize() failed, error code = {mt5.last_error()}")
Enter fullscreen mode Exit fullscreen mode

6. Validating Input Parameters

Issue:

Methods like send_limit_order and update_limit_order assume that input parameters are valid without performing checks.

Recommendation:

Add validation for input parameters to ensure they meet expected formats and constraints.

Code Example:

def send_limit_order(self, symbol, side, price, volume, magic=1000, comment=""):
    if side.lower() not in ['buy', 'sell']:
        raise ValueError("Invalid side. Use 'buy' or 'sell'.")
    if price <= 0:
        raise ValueError("Price must be positive.")
    if volume <= 0:
        raise ValueError("Volume must be positive.")

    # Rest of your method...
Enter fullscreen mode Exit fullscreen mode

7. Enhancing the tf_dict

Issue:

Your tf_dict maps timeframe strings to their MT5 constants and their equivalent in seconds. The comment suggests uncertainty about how timeframes represent days.

Recommendation:

Consider calculating the number of bars or days based on the timeframe and your specific use case. Additionally, you might want to include more descriptive information or helper methods to handle timeframe conversions.

Code Enhancement:

from enum import Enum

class TimeFrame(Enum):
    M1 = mt5.TIMEFRAME_M1
    M2 = mt5.TIMEFRAME_M2
    # ... other timeframes

self.tf_dict = {
    'M1': (TimeFrame.M1.value, 60),
    'M2': (TimeFrame.M2.value, 120),
    # ... other mappings
}
Enter fullscreen mode Exit fullscreen mode

This approach makes the timeframes more manageable and less error-prone.


8. Handling copy_rates_range and copy_rates_from_pos Responses

Issue:

In methods get_ohlc_range and get_ohlc_pos, if data_raw is empty, the method returns None implicitly. This can lead to unexpected NoneType errors downstream.

Recommendation:

Explicitly handle empty responses by returning an empty DataFrame or raising an exception, depending on your use case.

Code Correction:

def get_ohlc_range(self, symbol, timeframe, start_date, end_date=datetime.now()):
    tf = self.tf_dict.get(timeframe, [None])[0]
    if tf is None:
        raise ValueError(f"Invalid timeframe: {timeframe}")

    data_raw = mt5.copy_rates_range(symbol, tf, start_date, end_date) 
    if data_raw is None:
        logging.warning("No data retrieved for the given range.")
        return pd.DataFrame()

    df_data = self._format_ohlc(data_raw)
    return df_data if df_data is not None else pd.DataFrame()
Enter fullscreen mode Exit fullscreen mode

9. Improving the update_limit_order Method

Issue:

The update_limit_order method updates the order's price and volume but doesn't validate whether the order exists or whether the modification was successful.

Recommendation:

Check if the order exists before attempting to modify it and handle the response from order_send appropriately.

Code Enhancement:

def update_limit_order(self, order_id, price, volume):
    order = mt5.order_get(ticket=order_id)
    if order is None:
        logging.error(f"Order ID {order_id} not found.")
        return None

    request = {
        "action": mt5.TRADE_ACTION_MODIFY,
        "order": order_id,
        "price": price,
        "volume": volume,
    }
    result = mt5.order_send(request)
    if result.retcode != mt5.TRADE_RETCODE_DONE:
        logging.error(f"Failed to update order {order_id}, retcode={result.retcode}")
    return result._asdict()
Enter fullscreen mode Exit fullscreen mode

10. Ensuring Thread Safety

Issue:

If you plan to use AsimovMT in a multi-threaded environment, concurrent access to shared resources like self.positions and self.orders could lead to race conditions.

Recommendation:

Implement thread synchronization mechanisms (e.g., threading locks) to protect shared resources.

Code Example:

import threading

class AsimovMT:
    def __init__(self):
        # Existing initialization code...
        self.lock = threading.Lock()

    def check_positions_and_orders(self):
        with self.lock:
            new_positions = [i._asdict() for i in mt5.positions_get()]
            new_orders = [i._asdict() for i in mt5.orders_get()]

            check = (self.positions != new_positions) or (self.orders != new_orders)
            self.positions = new_positions
            self.orders = new_orders
            return check
Enter fullscreen mode Exit fullscreen mode

11. Finalizing MT5 Connection

Issue:

There’s no mechanism to properly shut down the MT5 connection when the program exits, which can lead to resource leaks.

Recommendation:

Implement a destructor method (__del__) or use context managers to ensure mt5.shutdown() is called appropriately.

Code Example Using Destructor:

def __del__(self):
    mt5.shutdown()
    logging.info("MetaTrader5 connection closed.")
Enter fullscreen mode Exit fullscreen mode

Or Using Context Manager:

from contextlib import contextmanager

@contextmanager
def asimov_mt_context():
    asimov_mt = AsimovMT()
    try:
        yield asimov_mt
    finally:
        del asimov_mt

# Usage
if __name__ == "__main__":
    with asimov_mt_context() as asimov_mt:
        # Your testing code here
Enter fullscreen mode Exit fullscreen mode

12. Enhancing Documentation and Readability

Issue:

The current code lacks docstrings and comments in English, which can hinder understanding and maintenance, especially for collaborators who may not speak Portuguese.

Recommendation:

Add docstrings to classes and methods, and ensure comments are in a consistent language (preferably English for broader accessibility).

Code Example:

class AsimovMT:
    """
    AsimovMT is a class that interfaces with MetaTrader5 to manage trading operations,
    retrieve market data, and monitor positions and orders.
    """

    def __init__(self):
        """
        Initializes the MetaTrader5 connection and retrieves current positions, orders,
        and historical data.
        """
        # Initialization code...
Enter fullscreen mode Exit fullscreen mode

13. Sample Revised Main Block

Here’s how your __main__ block could look after applying the recommendations:

if __name__ == "__main__":
    try:
        asimov_mt = AsimovMT()

        # Testing market data methods
        start_date = datetime.today() - timedelta(days=1)
        df_range = asimov_mt.get_ohlc_range('PETR4', 'M1', start_date, datetime.today())
        print(df_range.head())

        df_pos = asimov_mt.get_ohlc_pos('PETR4', 'M1', 0, 1000)
        print(df_pos.head())

        # Testing balance methods
        positions_changed = asimov_mt.check_positions_and_orders()
        orders_changed = asimov_mt.check_positions_and_orders()
        print("Positions Changed:", positions_changed)
        print("Orders Changed:", orders_changed)

        h_positions_changed = asimov_mt.check_h_positions_and_orders(0)
        print("Historical Positions Changed:", h_positions_changed)
        print("Historical Deals:", asimov_mt.h_deals)
        print("Historical Orders:", asimov_mt.h_orders)

        # Testing order methods
        symbol = 'ITSA4'
        side = 'buy'
        volume = 100.0  # Should be float
        market_buy = asimov_mt.send_market_order(symbol, side, volume, comment='test')
        print("Market Buy Order:", market_buy)

        market_sell = asimov_mt.send_market_order(symbol, 'sell', volume, comment='test')
        print("Market Sell Order:", market_sell)

        limit_buy_price = 7.80
        limit_buy = asimov_mt.send_limit_order(symbol, 'buy', limit_buy_price, volume, comment='test')
        print("Limit Buy Order:", limit_buy)

        limit_sell_price = 8.00
        limit_sell = asimov_mt.send_limit_order(symbol, 'sell', limit_sell_price, volume, comment='test')
        print("Limit Sell Order:", limit_sell)

        # Update and cancel limit sell order
        if limit_sell:
            updated_order = asimov_mt.update_limit_order(limit_sell["order"], 8.01, volume=100.0)
            print("Updated Order:", updated_order)

            cancelled_order = asimov_mt.cancel_limit_order(limit_sell["order"])
            print("Cancelled Order:", cancelled_order)

    except Exception as e:
        logging.exception("An error occurred during execution.")
Enter fullscreen mode Exit fullscreen mode

Conclusion

By addressing the issues and implementing the recommendations above, your AsimovMT class will become more robust, maintainable, and reliable. Proper error handling, input validation, and consistent naming conventions are crucial for developing scalable and professional-grade trading applications. Additionally, enhancing documentation and logging will aid in debugging and future development efforts.

Feel free to reach out if you have specific questions or need further assistance with particular aspects of your code!

Top comments (0)