Automated Trading with MetaTrader5: Order Management and Market Data Collection
Henrique Vital
Posted on October 3, 2024
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...
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()]
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
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.")
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()}")
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...
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
}
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()
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()
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
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.")
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
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...
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.")
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!
Posted on October 3, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
October 3, 2024