Русский 中文 Español Deutsch 日本語 Português
preview
Developing a trading robot in Python (Part 3): Implementing a model-based trading algorithm

Developing a trading robot in Python (Part 3): Implementing a model-based trading algorithm

MetaTrader 5Trading systems | 11 December 2024, 10:29
5 739 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

In the first article of the series, we loaded the dataset, placed labels, enriched the dataset and also performed dataset labeling. The second article was devoted to the creation and training of the model, as well as implementation of cross-validation and bagging. 

Now that our model is trained and tested, it is time to start real trading using the MetaTrader 5 library for Python. This powerful library allows us to automate trading directly through Python using the functions and classes provided by the MetaTrader 5 platform.


Implementation of a model-based trading algorithm

To implement a trading algorithm based on our model, we will use the following approach. The basic algorithm is to open trades with pre-set stop losses and take profits that match the labels generated by the model. If the model predicts the asset price rise, we open a long position with stop loss and take profit levels set. If the model predicts the asset price fall, we open a short position with similar stop loss and take profit parameters.

The MetaTrader 5 library for Python provides the necessary tools for managing the opening and closing of trades, as well as for setting stop loss and take profit levels. This allows us to fully automate trading based on the model forecasts.

Using the data obtained in the previous stages of analysis and training, we can transmit signals to open and close positions on the MetaTrader 5 platform in real time, thereby ensuring the continuity and accuracy of our trading algorithm.

Thus, integrating our trained model with the MetaTrader 5 library for Python allows us to create an efficient and automated trading algorithm that trades based on the model predictions, managing risks with preset stop losses and protecting profits with take profits.


Setting up the environment, trading terminal and running the algorithm

First, we need to set up the environment and the MetaTrader 5 trading terminal. To do this, follow these steps:

Install the Python libraries using the pip command:

pip install numpy pandas MetaTrader5 scikit-learn xgboost

Indicate our path to the terminal executable file in the links to the terminal:

terminal_path = "C:/Program Files/RoboForex - MetaTrader 5/Arima/terminal64.exe"


Implementing online trading

To implement online trading, we will add the online_trading function that will open trades according to our model forecast.

The online_trading function takes the following arguments:

  • symbol - traded symbol
  • features - list of features to be used to make forecasts
  • model - trained model to be used to make forecasts

Inside the online_trading function, we first connect to the MetaTrader 5 terminal using the path specified in terminal_path. Then we get the current symbol prices using the mt5.symbol_info_tick(symbol) function.

Next, we use our model to predict the signal based on the passed features. If the forecast is positive (more than 0.5), we open a long position, and if the forecast is negative (less than 0.5), we open a short position.

We also set stop loss and take profit for each trade to minimize risks.

If we have already reached the maximum number of open trades (set in the MAX_OPEN_TRADES constant), we do not open new ones and wait until one of the open trades is closed.

If the forecast does not allow us to open a new trade, we also wait until a new signal appears.

At the end of the function, we return the result of the trade execution if it was successfully placed, or None if a trade was not placed.

def online_trading(symbol, features, model):
    terminal_path = "C:/Program Files/RoboForex - MetaTrader 5/Arima/terminal64.exe"

    if not mt5.initialize(path=terminal_path):
        print("Error: Failed to connect to MetaTrader 5 terminal")
        return

    open_trades = 0
    e = None
    attempts = 30000

    while True:
        symbol_info = mt5.symbol_info(symbol)
        if symbol_info is not None:
            break
        else:
            print("Error: Instrument not found. Attempt {} of {}".format(_ + 1, attempts))
            time.sleep(5)

    while True:
        price_bid = mt5.symbol_info_tick(symbol).bid
        price_ask = mt5.symbol_info_tick(symbol).ask

        signal = model.predict(features)

        positions_total = mt5.positions_total()

        for _ in range(attempts):
            if positions_total < MAX_OPEN_TRADES and signal[-1] > 0.5:
                request = {
                    "action": mt5.TRADE_ACTION_DEAL,
                    "symbol": symbol,
                    "volume": 0.3,
                    "type": mt5.ORDER_TYPE_BUY,
                    "price": price_ask,
                    "sl": price_ask - 150 * symbol_info.point,
                    "tp": price_ask + 800 * symbol_info.point,
                    "deviation": 20,
                    "magic": 123456,
                    "comment": "Test deal",
                    "type_time": mt5.ORDER_TIME_GTC,
                    "type_filling": mt5.ORDER_FILLING_FOK,
                }
            elif positions_total < MAX_OPEN_TRADES and signal[-1] < 0.5:
                request = {
                    "action": mt5.TRADE_ACTION_DEAL,
                    "symbol": symbol,
                    "volume": 0.3,
                    "type": mt5.ORDER_TYPE_SELL,
                    "price": price_bid,
                    "sl": price_bid + 150 * symbol_info.point,
                    "tp": price_bid - 800 * symbol_info.point,
                    "deviation": 20,
                    "magic": 123456,
                    "comment": "Test deal",
                    "type_time": mt5.ORDER_TIME_GTC,
                    "type_filling": mt5.ORDER_FILLING_FOK,
                }
            else:
                print("No signal to open a position")
                return None

            result = mt5.order_send(request)

            if result.retcode == mt5.TRADE_RETCODE_DONE:
                if signal[-1] < 0.5:
                    print("Buy position opened")
                    open_trades += 1
                elif signal[-1] > 0.5:
                    print("Sell position opened")
                    open_trades += 1
                return result.order
            else:
                print("Error: Trade request not executed, retcode={}. Attempt {}/{}".format(result.retcode, _ + 1, attempts))
                time.sleep(3)

        time.sleep(4000)

def process_symbol(symbol):
    try:
        # Retrieve data for the specified symbol
        raw_data = retrieve_data(symbol)
        if raw_data is None:
            print("No data found for symbol {}".format(symbol))
            return None

        # Augment data
        augmented_data = augment_data(raw_data)

        # Markup data
        marked_data = markup_data(augmented_data.copy(), 'close', 'label')

        # Label data
        labeled_data = label_data(marked_data, symbol)

        # Generate new features
        labeled_data_generate = generate_new_features(labeled_data, num_features=100, random_seed=1)

        # Cluster features by GMM
        labeled_data_clustered = cluster_features_by_gmm(labeled_data_generate, n_components=4)

        # Feature engineering
        labeled_data_engineered = feature_engineering(labeled_data_clustered, n_features_to_select=10)

        # Train XGBoost classifier
        train_data = labeled_data_engineered[labeled_data_engineered.index <= FORWARD]


Launching online trading algorithm

To launch the online trading algorithm, we call the process_symbol function, while passing the traded symbol to it.

Inside the process_symbol function, we call the online_trading function, passing it the required arguments, and start a loop that will run until it is interrupted.

At the end of the loop, we make a pause of 6 seconds so as not to overload the MetaTrader 5 terminal.

If an error occurs during the algorithm execution, print an error message and abort the execution.


Intelligent risk and drawdown management system

Risk management is the cornerstone of a successful trading strategy. One of the main risks that traders face is the risk of drawdown, when asset prices fall below a predetermined level. To minimize this risk, an effective risk management system is required that is capable of adapting to changing market conditions.

We will develop a system that automatically reduces the volume of trades when the account balance draws down, using the tools of the MetaTrader 5 library for Python.

The basic rules of the system:

  1. Daily retrieval of current account balance - every day the system will retrieve the current account balance to analyze the changes.

  2. Reducing the volume of trades during a drawdown - if the balance has fallen by more than 2% in a day, the system will automatically reduce the volume of opened deals by 10%. This happens in 0.01 lot increments to gradually reduce risks.

  3. Additional volume reduction as balance continues to decline - if the balance continues to decline, then each day the volume of deals will be reduced by an additional 10% and by 0.01 lots, ensuring further capital protection.

  4. Revert to original volume after balance restoration - when the current account balance exceeds the previous peak, the system automatically returns the trading volume to the original value. This helps restore full trading activity after a temporary drop in volume due to a drawdown.

New variables have been added to the online_trading function: account_balance - to store the current balance, peak_balance - to store the previous peak balance level and daily_drop - to track the daily drawdown. These variables are used to implement risk management logic.

Sample code:

def online_trading(symbol, features, model):
    terminal_path = "C:/Program Files/RoboForex - MetaTrader 5/Arima/terminal64.exe"

    if not mt5.initialize(path=terminal_path):
        print("Error: Failed to connect to MetaTrader 5 terminal")
        return

    open_trades = 0
    e = None
    attempts = 30000

    # Get the current account balance
    account_info = mt5.account_info()
    account_balance = account_info.balance

    # Set the initial volume for opening trades
    volume = 0.3

    # Set the initial peak balance
    peak_balance = account_balance

    while True:
        symbol_info = mt5.symbol_info(symbol)
        if symbol_info is not None:
            break
        else:
            print("Error: Instrument not found. Attempt {} of {}".format(_ + 1, attempts))
            time.sleep(5)

    while True:
        price_bid = mt5.symbol_info_tick(symbol).bid
        price_ask = mt5.symbol_info_tick(symbol).ask

        signal = model.predict(features)

        positions_total = mt5.positions_total()

        # Calculate the daily drop in account balance
        account_info = mt5.account_info()
        current_balance = account_info.balance
        daily_drop = (account_balance - current_balance) / account_balance

        # Reduce the volume for opening trades by 10% with a step of 0.01 lot for each day of daily drop
        if daily_drop > 0.02:
            volume -= 0.01
            volume = max(volume, 0.01)
        elif current_balance > peak_balance:
            volume = 0.3
            peak_balance = current_balance

        for _ in range(attempts):
            if positions_total < MAX_OPEN_TRADES and signal[-1] > 0.5:
                request = {
                    "action": mt5.TRADE_ACTION_DEAL,
                    "symbol": symbol,
                    "volume": volume,
                    "type": mt5.ORDER_TYPE_BUY,
                    "price": price_ask,
                    "sl": price_ask - 150 * symbol_info.point,
                    "tp": price_ask + 800 * symbol_info.point,
                    "deviation": 20,
                    "magic": 123456,
                    "comment": "Test deal",
                    "type_time": mt5.ORDER_TIME_GTC,
                    "type_filling": mt5.ORDER_FILLING_FOK,
                }
            elif positions_total < MAX_OPEN_TRADES and signal[-1] < 0.5:
                request = {
                    "action": mt5.TRADE_ACTION_DEAL,
                    "symbol": symbol,
                    "volume": volume,
                    "type": mt5.ORDER_TYPE_SELL,
                    "price": price_bid,
                    "sl": price_bid + 150 * symbol_info.point,
                    "tp": price_bid - 800 * symbol_info.point,
                    "deviation": 20,
                    "magic": 123456,
                    "comment": "Test deal",
                    "type_time": mt5.ORDER_TIME_GTC,
                    "type_filling": mt5.ORDER_FILLING_FOK,
                }
            else:
                print("No signal to open a position")
                return None

            result = mt5.order_send(request)

            if result.retcode == mt5.TRADE_RETCODE_DONE:
                if signal[-1] < 0.5:
                    print("Buy position opened")
                    open_trades += 1
                elif signal[-1] > 0.5:
                    print("Sell position opened")
                    open_trades += 1
                return result.order
            else:
                print("Error: Trade request not executed, retcode={}. Attempt {}/{}".format(result.retcode, _ + 1, attempts))
                time.sleep(3)

        time.sleep(4000)


Lot management implementation based on Kelly criterion

Lot size management is another important aspect of risk management in trading. One of the popular methods of lot density control is the Kelly criterion. The Kelly Criterion is a mathematical equation that helps determine the optimal bet size based on the probability of winning and the ratio of wins to losses.

Here we will look at how to implement Kelly lot size management into our risk management system. We will use the distance between the model's prediction and 0.5 as the winning probability. The closer the model's prediction is to 0.5, the lower the probability of winning, and the smaller the bet size should be.

Here is what the modified online_trading function looks like:

def online_trading(symbol, features, model):
    terminal_path = "C:/Program Files/RoboForex - MetaTrader 5/Arima/terminal64.exe"

    if not mt5.initialize(path=terminal_path):
        print("Error: Failed to connect to MetaTrader 5 terminal")
        return

    open_trades = 0
    e = None
    attempts = 30000

    # Get the current account balance
    account_info = mt5.account_info()
    account_balance = account_info.balance

    # Set the initial volume for opening trades
    volume = 0.1

    # Set the initial peak balance
    peak_balance = account_balance

    while True:
        symbol_info = mt5.symbol_info(symbol)
        if symbol_info is not None:
            break
        else:
            print("Error: Instrument not found. Attempt {} of {}".format(_ + 1, attempts))
            time.sleep(5)

    while True:
        price_bid = mt5.symbol_info_tick(symbol).bid
        price_ask = mt5.symbol_info_tick(symbol).ask

        signal = model.predict(features)

        positions_total = mt5.positions_total()

        # Calculate the daily drop in account balance
        account_info = mt5.account_info()
        current_balance = account_info.balance
        daily_drop = (account_balance - current_balance) / account_balance

        # Calculate the probability of winning based on the distance between the model's prediction and 0.5
        probability_of_winning = abs(signal[-1] - 0.5) * 2

        # Calculate the optimal volume for opening trades using the Kelly criterion
        optimal_volume = (probability_of_winning - (1 - probability_of_winning) / risk_reward_ratio) / risk_reward_ratio * account_balance / price_ask

        # Reduce the volume for opening trades by 10% with a step of 0.01 lot for each day of daily drop
        if daily_drop > 0.02:
            optimal_volume -= 0.01
            optimal_volume = max(optimal_volume, 0.01)
        elif current_balance > peak_balance:
            optimal_volume = (probability_of_winning - (1 - probability_of_winning) / risk_reward_ratio) / risk_reward_ratio * account_balance / price_ask
            peak_balance = current_balance

        # Set the volume for opening trades
        volume = optimal_volume

        for _ in range(attempts):
            if positions_total < MAX_OPEN_TRADES and signal[-1] > 0.5:
                request = {
                    "action": mt5.TRADE_ACTION_DEAL,
                    "symbol": symbol,
                    "volume": volume,
                    "type": mt5.ORDER_TYPE_BUY,
                    "price": price_ask,
                    "sl": price_ask - 150 * symbol_info.point,
                    "tp": price_ask + 800 * symbol_info.point,
                    "deviation": 20,
                    "magic": 123456,
                    "comment": "Test deal",
                    "type_time": mt5.ORDER_TIME_GTC,
                    "type_filling": mt5.ORDER_FILLING_FOK,
                }
            elif positions_total < MAX_OPEN_TRADES and signal[-1] < 0.5:
                request = {
                    "action": mt5.TRADE_ACTION_DEAL,
                    "symbol": symbol,
                    "volume": volume,
                    "type": mt5.ORDER_TYPE_SELL,
                    "price": price_bid,
                    "sl": price_bid + 150 * symbol_info.point,
                    "tp": price_bid - 800 * symbol_info.point,
                    "deviation": 20,
                    "magic": 123456,
                    "comment": "Test deal",
                    "type_time": mt5.ORDER_TIME_GTC,
                    "type_filling": mt5.ORDER_FILLING_FOK,
                }
            else:
                print("No signal to open a position")
                return None

            result = mt5.order_send(request)

            if result.retcode == mt5.TRADE_RETCODE_DONE:
                if signal[-1] < 0.5:
                    print("Buy position opened")
                    open_trades += 1
                elif signal[-1] > 0.5:
                    print("Sell position opened")
                    open_trades += 1
                return result.order
            else:
                print("Error: Trade request not executed, retcode={}. Attempt {}/{}".format(result.retcode, _ + 1, attempts))
                time.sleep(3)

        time.sleep(4000)

In this function, I added the probability_of_winning and optimal_volume variables. probability_of_winning stores the probability of winning calculated as a distance between model forecast and 0.5 multiplied by 2. optimal_volume contains the optimal bet size calculated using the Kelly criterion.

In the 'if daily_drop > 0.05:' block, I decrease optimal_volume by 10% in increments of 0.01 lot. I also added the 'elif current_balance > peak_balance:' condition, which checks whether the current account balance has exceeded the previous peak level. If yes, I recalculate optimal_volume for the current balance and win/loss ratio, while peak_balance is updated.

This Kelly lot size management system will allow me to automatically optimize my bet size based on the probability of winning and the win/loss ratio. This way I can maximize profits and minimize the risk of drawdown.


Implementation of multi-currency operation and parallel computing

Multi-currency operation allows the algorithm to trade several currency pairs at once, diversifying risks. It is implemented via the 'symbols' list with pairs.

Parallel computing speeds up work by performing tasks simultaneously. We use the 'threading' library - a separate thread is created for each pair with the process_symbol function.

process_symbol downloads the pair data, transforms it, trains the XGBoost model, tests it, and then trades it online, updating the data periodically.

Threads are created for all pairs from 'symbols'. After all threads have completed, the program waits for them using thread.join().

Thus, a high-performance multi-threaded multi-currency trading system based on XGBoost is implemented.

import threading

def process_symbol(symbol):
    try:
        # Retrieve data for the specified symbol
        raw_data = retrieve_data(symbol)
        if raw_data is None:
            print("No data found for symbol {}".format(symbol))
            return None

        # Augment data
        augmented_data = augment_data(raw_data)

        # Markup data
        marked_data = markup_data(augmented_data.copy(), 'close', 'label')

        # Label data
        labeled_data = label_data(marked_data, symbol)

        # Generate new features
        labeled_data_generate = generate_new_features(labeled_data, num_features=100, random_seed=1)

        # Cluster features by GMM
        labeled_data_clustered = cluster_features_by_gmm(labeled_data_generate, n_components=4)

        # Feature engineering
        labeled_data_engineered = feature_engineering(labeled_data_clustered, n_features_to_select=10)

        # Train XGBoost classifier
        train_data = labeled_data_engineered[labeled_data_engineered.index <= FORWARD]
        test_data = labeled_data_engineered[labeled_data_engineered.index > FORWARD]
        xgb_clf = train_xgboost_classifier(train_data, num_boost_rounds=1000)

        # Test XGBoost classifier
        test_features = test_data.drop(['label', 'labels'], axis=1)
        test_labels = test_data['labels']
        initial_balance = 10000.0
        markup = 0.00001
        test_model(xgb_clf, test_features, test_labels, markup, initial_balance)

        # Online trading
        position_id = None
        while True:
            # Get the last 2000 data points for online trading
            features = raw_data[-6000:].drop(['label', 'labels'], axis=1).values.tolist()

            # Update features every 6 seconds
            time.sleep(6)
            new_data = retrieve_data(symbol)
            if new_data is not None:
                raw_data = pd.concat([raw_data, new_data])
                raw_data = raw_data.dropna()

            # Online trading
            position_id = online_trading(symbol, features, xgb_clf, position_id)

    except Exception as e:
        print("Error processing {} symbol: {}".format(symbol, e))
        return None

symbols = ["EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "USDCAD"]

# Create a list of threads for each symbol
threads = []
for symbol in symbols:
    thread = threading.Thread(target=process_symbol, args=(symbol,))
    thread.start()
    threads.append(thread)

# Wait for all threads to complete
for thread in threads:
    thread.join()

After running the code, we encountered a problem with overlapping screen output (prints) from different threads. This makes the output on the screen unreadable and makes it difficult to debug and monitor the algorithm operation. Despite numerous attempts to solve this problem, it has not yet been possible to avoid output overlap.

Instruments in terminal: 31Instruments in terminal: 31Instruments in terminal: 31Instruments in terminal: 31Instruments in terminal: 31




No data for symbol USDCAD yet (attempt 1)No data for symbol NZDUSD yet (attempt 1)No data for symbol AUDUSD yet (attempt 1)
No data for symbol GBPUSD yet (attempt 1)


Instruments in terminal: 31Instruments in terminal: 31Instruments in terminal: 31Instruments in terminal: 31



No data for symbol USDCAD yet (attempt 2)No data for symbol AUDUSD yet (attempt 2)No data for symbol GBPUSD yet (attempt 2)


Instruments in terminal: 31Instruments in terminal: 31

Instruments in terminal: 31
No data for symbol GBPUSD yet (attempt 3)
Instruments in terminal: 31

Despite this problem, the algorithm itself works correctly and performs its functions. It successfully handles the data, trains the model and performs trading for each currency pair from the 'symbols' list. Parallel computing allows to significantly increase the speed of the algorithm and improve its efficiency.

So, we see that the algorithm opens trades correctly:



Potential further system improvement

Quantum machine learning

We currently use classical model training, but in the future we plan to move to quantum machine learning using qubits to improve accuracy and speed. Despite the current difficulties with quantum computers, special software and algorithms are already being developed. I plan to research and implement quantum learning into our system, which could be a significant breakthrough.

Market feedback: reinforcement learning

To improve the trading system, we can implement reinforcement learning. The system will act as an agent interacting with the market, receiving rewards for profitable trades and penalties for unprofitable ones. Using algorithms, such as DQN, the system will learn and adapt, improving its forecasts.

Swarm intelligence for selecting stops and takes

To improve the efficiency of our system, we can apply swarm intelligence using the Particle Swarm Optimization (PSO) algorithm. The system will generate various stop loss and take profit parameters, evaluating their efficiency on historical data. PSO will help us select the optimal parameters, improving adaptability and reducing risks.


Conclusion

Our trading machine learning model uses data preprocessing, data analysis and XGBoost type algorithms. We also apply noise addition, time shifting, feature calculation, class balancing, and cross-validation. The accuracy is about 60% on test data.

Quantum computing, reinforcement learning, as well as swarm intelligence for choosing stop losses and take profits are to be implemented later. This will improve the quality of training, efficiency on real data and risk management.

But there is one catch - these technologies will create fierce competition between algorithms on the market. Previously, traders competed with each other, now it is more about whose algorithm is better.


Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/15127

Neural Network in Practice: Pseudoinverse (I) Neural Network in Practice: Pseudoinverse (I)
Today we will begin to consider how to implement the calculation of pseudo-inverse in pure MQL5 language. The code we are going to look at will be much more complex for beginners than I expected, and I'm still figuring out how to explain it in a simple way. So for now, consider this an opportunity to learn some unusual code. Calmly and attentively. Although it is not aimed at efficient or quick application, its goal is to be as didactic as possible.
Neural Networks Made Easy (Part 95): Reducing Memory Consumption in Transformer Models Neural Networks Made Easy (Part 95): Reducing Memory Consumption in Transformer Models
Transformer architecture-based models demonstrate high efficiency, but their use is complicated by high resource costs both at the training stage and during operation. In this article, I propose to get acquainted with algorithms that allow to reduce memory usage of such models.
Portfolio Risk Model using Kelly Criterion and Monte Carlo Simulation Portfolio Risk Model using Kelly Criterion and Monte Carlo Simulation
For decades, traders have been using the Kelly Criterion formula to determine the optimal proportion of capital to allocate to an investment or bet to maximize long-term growth while minimizing the risk of ruin. However, blindly following Kelly Criterion using the result of a single backtest is often dangerous for individual traders, as in live trading, trading edge diminishes over time, and past performance is no predictor of future result. In this article, I will present a realistic approach to applying the Kelly Criterion for one or more EA's risk allocation in MetaTrader 5, incorporating Monte Carlo simulation results from Python.
Creating a Trading Administrator Panel in MQL5 (Part VIII): Analytics Panel Creating a Trading Administrator Panel in MQL5 (Part VIII): Analytics Panel
Today, we delve into incorporating useful trading metrics within a specialized window integrated into the Admin Panel EA. This discussion focuses on the implementation of MQL5 to develop an Analytics Panel and highlights the value of the data it provides to trading administrators. The impact is largely educational, as valuable lessons are drawn from the development process, benefiting both upcoming and experienced developers. This feature demonstrates the limitless opportunities this development series offers in equipping trade managers with advanced software tools. Additionally, we'll explore the implementation of the PieChart and ChartCanvas classes as part of the continued expansion of the Trading Administrator panel’s capabilities.