English Русский Español Deutsch 日本語 Português
preview
开发Python交易机器人(第三部分):实现基于模型的交易算法

开发Python交易机器人(第三部分):实现基于模型的交易算法

MetaTrader 5交易系统 | 10 一月 2025, 10:25
1 041 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

本系列文章的第一篇中,我们加载了数据集,进行了标签设置,丰富了数据集,并完成了数据集的标签工作。第二篇文章则专注于模型的创建与训练,以及交叉验证和袋装法(bagging)的实现。 

至此,我们的模型已经训练并测试完毕,是时候开始利用Python的MetaTrader 5库进行实际交易了。这个强大的库允许我们直接使用MetaTrader 5平台提供的函数和类,通过Python实现交易的自动化。


基于模型的交易算法实现

为了实现基于我们模型的交易算法,我们将采用以下方法。基本算法是根据模型生成的标签,以预设的止损和止盈水平开设交易。如果模型预测资产价格上涨,我们将开设一个多头仓位,并设定止损和止盈水平。如果模型预测资产价格下跌,我们将开设一个空头仓位,并设置类似的止损和止盈参数。

MetaTrader 5的Python库提供了管理交易开仓和平仓所需的工具,以及设置止损和止盈水平的工具。这使得我们能够基于模型预测从而完全实现自动化交易。

利用在之前分析和训练阶段获得的数据,我们可以实时向MetaTrader 5平台发送开仓和平仓的信号,从而确保我们交易算法的连续性和准确性。

因此,将我们的训练模型与MetaTrader 5的 Python库相结合,创建一个高效且自动化的交易算法,该算法基于模型预测进行交易,通过预设止损来管理风险,并通过止盈来保护利润。


设置环境、交易终端和运行算法

首先,我们需要设置环境和MetaTrader 5交易终端。为此,请按照以下步骤操作:

使用pip命令安装Python库:

pip install numpy pandas MetaTrader5 scikit-learn xgboost

在指向终端的链接中显示到达终端可执行文件的路径:

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


实现在线交易

为了实现在线交易,我们将添加一个online_trading函数,该函数将根据我们的模型预测来开仓。

online_trading函数接受以下参数:

  • symbol - 交易品种
  • features - 用于进行预测的特征列表
  • model - 用于进行预测的已训练模型

在online_trading函数内部,我们首先使用在terminal_path中指定的路径连接到MetaTrader 5终端。然后,我们使用mt5.symbol_info_tick(symbol)函数获取当前交易品种的价格。

接下来,我们基于传递的特征使用模型来预测信号。如果预测结果是正面的(大于0.5),我们就开立多头仓位;如果预测结果是负面的(小于0.5),我们就开立空头仓位。

我们还为每个交易设置了止损和止盈,以最小化风险。

如果已经达到了最大开仓数量(在MAX_OPEN_TRADES常量中设置),我们就不会开立新的仓位,而是等到其中一个已开立的仓位被平仓。

如果预测结果不允许我们开立新仓,我们也会等待,直到出现新的信号。

在函数末尾,如果交易成功执行,我们返回交易结果;如果没有进行交易,则返回None。

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]


启动在线交易算法

为了启动在线交易算法,我们调用process_symbol函数,并向其传递交易品种。

在process_symbol函数内部,我们调用online_trading函数,向其传递所需的参数,并启动一个循环,该循环将持续运行直到被中断为止。

在循环的末尾,我们暂停6秒钟,以避免给MetaTrader 5终端带来过大的负荷。

如果算法在执行过程中出现错误,打印错误消息并中止执行。


智能风险与回撤管理系统

风险管理是成功交易策略的基石。交易者面临的主要风险之一是回撤风险,即资产价格跌至预定水平以下。为了最小化这种风险,需要一个有效的风险管理系统,该系统能够适应不断变化的市场条件。

我们将开发一个系统,该系统在账户余额回撤时自动减少交易量,利用MetaTrader 5的Python库工具来实现。

系统的基本规则:

  1. 每日检索当前账户余额——系统每天都会检索当前账户余额用以分析市场变化。

  2. 回撤期间减少交易量——如果一天内余额下降超过2%,系统将自动将已开仓的交易量减少10%。这将以0.01手的增量逐步减少风险。

  3. 随着余额持续下降额外交易量也随之缩减——如果余额持续减少,那么每天的交易量将额外减少10%,并减少0.01手,以确保进一步的资本保护。

  4. 余额恢复后交易量恢复原状——当目前账户余额超过之前的峰值时,系统会自动将交易量恢复到原始值。这有助于在交易量因回撤而暂时下降后恢复全面的交易活动。

已在online_trading函数中添加了新变量:account_balance——用于存储当前余额,peak_balance——用于存储之前的峰值余额水平,以及daily_drop——用于跟踪每日回撤。用这些变量来实现风险管理逻辑。

代码示例:

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)


基于凯利公式的仓位管理

仓位管理是交易风险管理的另一个重要方面。仓位密度控制的流行方法之一就是凯利公式。凯利公式是一个数学方程,它根据获胜的概率以及盈亏比例来确定最佳下注金额。

在这里,我们将探讨如何将凯利仓位管理纳入我们的风险管理系统。我们将使用模型预测值与0.5之间的距离作为获胜概率。模型的预测值越接近0.5,获胜的概率就越低,相应的下注金额也应该越小。

以下是修改后的online_trading函数的示例:

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)

在这个函数中,我添加了probability_of_winning和optimal_volute变量。 probability_of_winning存储获胜概率,该概率通过模型预测与0.5之间的距离乘以2来计算。optimal_volume包含使用Kelly公式计算得出的最佳下注金额。

在'if daily_drop > 0.05:'代码块中,我通过每次减少0.01手的方式,将optimal_volume(最佳下注金额)降低10%。我还添加了'elif current_balance > peak_balance:'条件,用于检查当前账户余额是否超过了之前的峰值水平。如果超过了,我会根据当前余额和盈亏比例重新计算optimal_volume,并更新peak_balance。

这个基于凯利的仓位管理系统将允许我根据获胜概率和盈亏比例自动优化下注金额。这样一来,我可以最大化利润,同时最小化回撤风险。


多币种操作和并行计算的实现

多币种操作允许算法同时交易多个货币对,从而分散风险。这是通过包含货币对的'symbols'列表来实现的。

并行计算通过同时执行任务来加快工作速度。我们使用'threading'库——为每个货币对创建一个单独的线程,并为其分配process_symbol函数。

process_symbol函数负责下载货币对的数据,对数据进行转换,训练XGBoost模型,测试模型,然后在线交易,并定期更新数据。

为'symbols'列表中的所有货币对创建线程。所有线程完成后,程序使用thread.join()等待就绪。

由此,我们实现了一个基于XGBoost的高性能多线程多币种交易系统。

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()

运行代码后,我们遇到了不同线程的屏幕输出(打印信息)重叠的问题。这导致屏幕上的输出难以阅读,并且使得调试和监控算法运行变得困难。尽管我们尝试了多种方法来解决这个问题,但至今仍未能避免输出重叠。

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

尽管存在这个问题,但算法本身工作正常,能够执行其功能。其能够成功地处理数据,训练模型,并为“symbols”列表中的每一对货币执行交易。并行计算可以显著提高算法的速度并改善其效率。

因此,算法能够正确地开启交易:



潜在且逐步的系统改进

量子机器学习

我们目前使用经典模型进行训练,但未来计划转向使用量子比特进行量子机器学习,以提高准确性和速度。尽管量子计算机目前还存在困难,但已经有专门的软件和算法正在开发中。我计划研究并将量子学习应用到我们的系统中,这可能会是一个重大的突破。

市场反馈:强化学习

为了改进交易系统,我们可以强化学习。系统将作为与市场交互的代理,通过盈利交易获得奖励,通过亏损交易受到惩罚。使用如深度Q网络(DQN)等算法,系统将学习和适应,从而提高其预测能力。

用于选择止损和止盈的群体智能

为了提高我们系统的效率,我们可以使用粒子群优化(PSO)算法来实现群体智能。系统将生成各种止损和止盈参数,并基于历史数据评估它们的效率。PSO将帮助我们选择最优参数,提高系统的适应性并降低风险。


结论

我们的交易机器学习模型采用了数据预处理、数据分析以及XGBoost类算法。我们还应用了噪声添加、时间平移、特征计算、类别平衡和交叉验证等技术。在测试数据上,模型的准确率约为60%。

量子计算、强化学习以及用于选择止损和止盈的群体智能技术将在后续实现。这将提高训练质量、在真实数据上的效率以及风险管理水平。

但还有一个问题——这些技术将在市场上引发算法之间的激烈竞争。以前,交易者之间是人与人的竞争,而现在更多则是谁的算法更优秀。


本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/15127

您应当知道的 MQL5 向导技术(第 16 部分):配合本征向量进行主成分分析 您应当知道的 MQL5 向导技术(第 16 部分):配合本征向量进行主成分分析
本文所见的主成分分析,是数据分析中的一种降维技术,文中还有如何配合本征值和向量来实现它。一如既往,我们瞄向的是开发一个可在 MQL5 向导中使用的原型专业信号类。
开发回放系统(第 51 部分):事情变得复杂(三) 开发回放系统(第 51 部分):事情变得复杂(三)
在本文中,我们将研究 MQL5 编程领域最困难的问题之一:如何正确获取图表 ID,以及为什么对象有时不会绘制在图表上。此处提供的材料仅用于教学目的,在任何情况下,除了学习和掌握所提出的概念外,都不应出于任何目的使用此应用程序。
开发多币种 EA 交易(第 12 部分):开发自营交易级别风险管理器 开发多币种 EA 交易(第 12 部分):开发自营交易级别风险管理器
在正在开发的 EA 中,我们已经有了某种控制回撤的机制。但它具有概率性,因为它是以历史价格数据的测试结果为基础的。因此,回撤有时会超过最大预期值(尽管概率很小)。让我们试着增加一种机制,以确保遵守指定的回撤水平。
MacOS 上的 MetaTrader 5 MacOS 上的 MetaTrader 5
我们为 macOS 上的 MetaTrader 5 交易平台提供了专用的安装程序。它是一个功能齐全的向导,允许您以本机方式安装应用程序。安装程序执行所有必需的步骤:它识别您的系统,下载并安装最新的 Wine 版本,对其进行配置,然后在其中安装 MetaTrader。所有步骤都在自动模式下完成,您可以在安装后立即开始使用平台。