English Русский Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
preview
使用 MetaTrader 5 的 Python 高频套利交易系统

使用 MetaTrader 5 的 Python 高频套利交易系统

MetaTrader 5交易 |
1 015 8
Yevgeniy Koshtenko
Yevgeniy Koshtenko

概述

外汇市场,算法策略,Python 和 MetaTrader 5。当我开始研究套利交易系统时,这一切就发生了。这个想法很简单 —— 创建一个高频系统来发现价格不平衡。这一切最终导致了什么?

这段时间我经常使用 MetaTrader 5 API。我决定计算合成交叉汇率。我决定不再将自己限制在十个或一百个,数字已突破一千。

风险管理是一项独立的任务。系统架构、算法、决策 —— 我们将在这里分析一切。我将展示回溯测试和实时交易的结果。当然,我也会分享未来的思路。谁知道呢,也许你们中有人想进一步发展这个话题?我希望我的工作会受到欢迎。我相信这将有助于算法交易的发展。也许有人会以此为基础,在高频套利的世界里创造出更有效的东西。毕竟,这就是科学的本质 —— 在前人经验的基础上不断前进。让我们直接进入正题。


外汇套利交易简介

让我们弄清楚它到底是什么。

货币兑换可以做一个类比。假设你可以在一个地方用欧元买入美元,在另一个地方立即用英镑卖出,然后用英镑兑换欧元,最终获利。这是最简单形式的套利。

事实上,情况要稍微复杂一些。外汇是一个巨大的、去中心化的市场。这里有很多银行、经纪商、基金。每个成员都有自己的汇率。他们往往不匹配。这就是我们套利的机会。但不要以为这是一笔容易赚的钱。通常,这些价格差异只会持续几秒钟,甚至是几毫秒。几乎不可能及时完成。这需要强大的计算机和快速的算法。

套利也有不同的类型。一个简单的例子是,我们从不同地方的利率差异中获利。当我们使用交叉汇率时,情况会变得复杂。例如,我们计算英镑在美元和欧元中的成本,并将其与英镑/欧元的直接汇率进行比较。

列表还不止于此,还有时间套利。在这里,我们从不同时间点的价格差异中获利。现在买入,一分钟内卖出。当然,这个过程似乎很简单。但主要的问题是,我们不知道价格在一分钟内会走向何方。这些是主要风险。市场逆转的速度可能快于您激活所需订单的速度。或者您的经纪商可能会延迟执行订单。总的来说,有很多困难和风险。尽管困难重重,外汇套利是一个相当受欢迎的系统。这里涉及大量的金融资源,并且有足够多的交易员专门从事这类交易。

现在,经过简短的介绍后,让我们开始讨论我们的策略。


所用技术概述:Python 和 MetaTrader 5

就是 Python 和 MetaTrader 5。 

Python 是一种通用且易于理解的编程语言。新手和经验丰富的开发人员都喜欢它,这并非没有道理。它最适合数据分析。

另一个是 MetaTrader 5。这是一个每个外汇交易者都熟悉的平台。它可靠且不复杂,它也很实用 —— 实时报价、交易机器人和技术分析。全部功能包含在一个应用程序中。为了取得积极成果,我们需要将所有这些结合起来。

Python 从 MetaTrader 5 获取数据,使用其库处理数据,然后将命令发送回 MetaTrader 5 以执行交易。当然,也有困难。但这些应用程序结合在一起非常高效。

有特别为开发人员提供的特殊库可用于通过 Python 使用 MetaTrader 5。要激活它,您只需安装它。完成此操作后,我们能够接收报价、发送订单和管理头寸。一切都与终端本身相同,只是现在也使用了 Python 功能。

我们现在有哪些功能和特性?现在有很多。例如,我们能够自动化交易并对历史数据进行复杂的分析。我们甚至可以创建自己的交易平台。这已经是高级用户的任务,但也是可能的。


设置环境:安装必要的库并连接到 MetaTrader 5

我们将使用 Python 开始我们的工作流程。如果您还没有,请访问 python.org。您还需要设置“添加到补丁”同意。

我们的下一步是库,我们需要其中的一些,主要的是 MetaTrader 5。安装不需要任何特殊技能。

打开命令行并输入:

pip install MetaTrader5 pandas numpy

按下 Enter 键然后去喝点咖啡,或者茶。或者任何你喜欢的。

一切都准备好了吗?现在是时候连接到 MetaTrader 5 了。

您需要做的第一件事就是安装 MetaTrader 5 本身。从您的经纪商处下载它。一定要记住终端的路径,通常,它看起来像这样:“C:\ProgramFiles\MetaTrader 5\terminal64.exe”。

现在打开 Python 并输入:

import MetaTrader5 as mt5

if not mt5.initialize(path="C:/Program Files/MetaTrader 5/terminal64.exe"):
    print("Alas! Failed to connect :(")
    mt5.shutdown()
else:
    print("Hooray! Connection successful!")

如果一切正常,请继续下一部分。


代码结构:主要函数及其用途

让我们从 “imports” 开始。这里我们有导入,例如:MetaTrader5、pandas、datetime、pytz…… 接下来是函数。

  • 第一个函数是 remove_duplicate_indices。它确保我们的数据中没有重复。
  • 接下来是get_mt5_data。它访问 MetaTrader 5 函数并提取过去 24 小时所需的数据。
  • get_currency_data — 非常有趣的函数。它调用 get_mt5_data 获取一组货币对。例如 AUDUSD、EURUSD、GBPJPY 以及更多货币对。
  • 下一个是 calculate_synthetic_prices。这个功能是一个真正的成就,它在处理货币对时会产生数百种合成价格。
  • analyze_arbitrage 通过比较实际价格和合成价格来寻找套利机会。所有发现都保存在 CSV 文件中。
  • open_test_limit_order — 另一个强大的代码单元。当发现套利机会时,此函数会开立测试订单。但同时最多可开设 10 个交易。

最后是 “main” 函数。它通过以正确的顺序调用函数来管理整个过程。

一切都以无限循环结束。它每 5 分钟运行一次整个循环,但仅在工作时间运行。这就是我们的结构,它简单但有效。 


从 MetaTrader 5 获取数据:get_mt5_data 函数

第一个任务是从终端接收数据。

if not mt5.initialize(path=terminal_path):
    print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}")
    return None
timezone = pytz.timezone("Etc/UTC")
utc_from = datetime.now(timezone) - timedelta(days=1)

请注意,我们使用 UTC。因为在外汇世界里,没有时区混淆的余地。

现在最重要的是得到分时报价:

ticks = mt5.copy_ticks_from(symbol, utc_from, count, mt5.COPY_TICKS_ALL)

数据已收到?太好了!现在我们需要处理它。为此,我们使用 Pandas:

ticks_frame = pd.DataFrame(ticks)
ticks_frame['time'] = pd.to_datetime(ticks_frame['time'], unit='s')

瞧!现在我们有了自己的包含数据的 DataFrame。它已经准备好进行分析了。

但如果出了问题怎么办?不用担心!我们的函数也涵盖了这一点:

if ticks is None:
    print(f"Failed to fetch data for {symbol}")
    return None

它只会报告问题并返回 None。


处理多个货币对:get_currency_data 函数

我们进一步深入研究系统 —— get_currency_data 函数。我们来看看代码:

def get_currency_data():
    # Define currency pairs and the amount of data
    symbols = ["AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"]
    count = 1000  # number of data points for each currency pair
    data = {}
    for symbol in symbols:
        df = get_mt5_data(symbol, count, terminal_path)
        if df is not None:
            data[symbol] = df[['time', 'bid', 'ask']].set_index('time')
    return data

一切都从定义货币对开始。该列表包括 AUDUSD、EURUSD、GBPJPY 和我们熟知的其他工具。

现在我们进入下一步。该函数创建一个空的 “data” 字典,稍后还将填充必要的数据。

现在我们的函数开始发挥作用。它将浏览货币对列表,对于每一对,它调用 get_mt5_data。如果 get_mt5_data 返回数据(而不是 None),我们的函数只会采用最重要的数据:时间、买入价和卖出价。

最后,这是大结局。该函数返回一个填充有数据的字典。 

现在我们得到 get_currency_data。它体积小,功能强大,简单但有效。


2000 个合成价格计算:策略与实施

我们深入研究我们系统的基础 —— calculate_synthetic_prices 函数。它使我们能够获得合成数据。

我们来看看代码:

def calculate_synthetic_prices(data):
    synthetic_prices = {}

    # Remove duplicate indices from all DataFrames in the data dictionary
    for key in data:
        data[key] = remove_duplicate_indices(data[key])

    # Calculate synthetic prices for all pairs using multiple methods
    pairs = [('AUDUSD', 'USDCHF'), ('AUDUSD', 'NZDUSD'), ('AUDUSD', 'USDJPY'),
             ('USDCHF', 'USDCAD'), ('USDCHF', 'NZDCHF'), ('USDCHF', 'CHFJPY'),
             ('USDJPY', 'USDCAD'), ('USDJPY', 'NZDJPY'), ('USDJPY', 'GBPJPY'),
             ('NZDUSD', 'NZDCAD'), ('NZDUSD', 'NZDCHF'), ('NZDUSD', 'NZDJPY'),
             ('GBPUSD', 'GBPCAD'), ('GBPUSD', 'GBPCHF'), ('GBPUSD', 'GBPJPY'),
             ('EURUSD', 'EURCAD'), ('EURUSD', 'EURCHF'), ('EURUSD', 'EURJPY'),
             ('CADCHF', 'CADJPY'), ('CADCHF', 'GBPCAD'), ('CADCHF', 'EURCAD'),
             ('CHFJPY', 'GBPCHF'), ('CHFJPY', 'EURCHF'), ('CHFJPY', 'NZDCHF'),
             ('NZDCAD', 'NZDJPY'), ('NZDCAD', 'GBPNZD'), ('NZDCAD', 'EURNZD'),
             ('NZDCHF', 'NZDJPY'), ('NZDCHF', 'GBPNZD'), ('NZDCHF', 'EURNZD'),
             ('NZDJPY', 'GBPNZD'), ('NZDJPY', 'EURNZD')]

    method_count = 1
    for pair1, pair2 in pairs:
        print(f"Calculating synthetic price for {pair1} and {pair2} using method {method_count}")
        synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['bid'] / data[pair2]['ask']
        method_count += 1
        print(f"Calculating synthetic price for {pair1} and {pair2} using method {method_count}")
        synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['bid'] / data[pair2]['bid']
        method_count += 1

    return pd.DataFrame(synthetic_prices)


分析套利机会:analyze_arbitrage 函数

首先,我们创建一个空字典 synthesize_prices。我们还将用数据填充它。然后,我们将遍历所有数据并删除重复的索引,以避免将来出现错误。

下一步是 “pairs” 列表,这些是我们将用于合成的货币对。然后另一个过程开始,我们对全部对进行循环。对于每一货币对,我们通过两种方式计算合成价格:

  1. 将第一对的买入价除以第二对的卖出价。
  2. 将第一对的卖出价除以第二对的买入价。

每次我们都增加我们的 method_count。 结果,我们得到了 2000 对合成货币对!

这就是 calculate_synthetic_prices 函数的工作原理。它不仅计算价格,而且实际上创造了新的机会。此功能以套利机会的形式提供了出色的结果!


可视化结果:将数据保存为 CSV

让我们看一下 analyze_arbitrage 函数。它不仅分析数据,还在一系列数字中搜索所需内容。让我们看一下:

def analyze_arbitrage(data, synthetic_prices, method_count):
    # Calculate spreads for each pair
    spreads = {}
    for pair in data.keys():
        for i in range(1, method_count + 1):
            synthetic_pair = f'{pair}_{i}'
            if synthetic_pair in synthetic_prices.columns:
                print(f"Analyzing arbitrage opportunity for {synthetic_pair}")
                spreads[synthetic_pair] = data[pair]['bid'] - synthetic_prices[synthetic_pair]
    # Identify arbitrage opportunities
    arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008
    print("Arbitrage opportunities:")
    print(arbitrage_opportunities)
    # Save the full table of arbitrage opportunities to a CSV file
    arbitrage_opportunities.to_csv('arbitrage_opportunities.csv')
    return arbitrage_opportunities

首先,我们的函数创建一个空的 “spreads” 字典。我们还将用数据填充它。

让我们继续下一步。该函数遍历所有货币对及其合成类似物。对于每一对货币,它都会计算价差 —— 即实际买入价和合成价格之间的差额。

spreads[synthetic_pair] = data[pair]['bid'] - synthetic_prices[synthetic_pair]

这一条起着相当重要的作用,它发现了真实价格和合成价格之间的差异。如果这个差额为正,我们就有套利机会。

为了获得更正式的结果,我们使用 0.00008 这个数字:

arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008

这个字符串把小于 8 个点的所有可能性都排序出来了。这样我们就会获得更高概率的获利机会。

下一步是:

arbitrage_opportunities.to_csv('arbitrage_opportunities.csv')

现在我们所有的数据都保存到 CSV 文件中。现在我们可以研究它们、分析它们、绘制图表 —— 总之,做富有成效的工作。所有这一切都得益于以下函数 —— analyze_arbitrage。它不只是进行分析,还寻找、发现并保存套利机会。


开启测试订单:open_test_limit_order 函数

接下来,让我们探讨 open_test_limit_order 函数。它将为我们打开订单。

让我们来看看:

def open_test_limit_order(symbol, order_type, price, volume, take_profit, stop_loss, terminal_path):
    if not mt5.initialize(path=terminal_path):
        print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}")
        return None
    symbol_info = mt5.symbol_info(symbol)
    positions_total = mt5.positions_total()
    if symbol_info is None:
        print(f"Instrument not found: {symbol}")
        return None
    if positions_total >= MAX_OPEN_TRADES:
        print("MAX POSITIONS TOTAL!")
        return None
    # Check if symbol_info is None before accessing its attributes
    if symbol_info is not None:
        request = {
            "action": mt5.TRADE_ACTION_DEAL,
            "symbol": symbol,
            "volume": volume,
            "type": order_type,
            "price": price,
            "deviation": 30,
            "magic": 123456,
            "comment": "Stochastic Stupi Sustem",
            "type_time": mt5.ORDER_TIME_GTC,
            "type_filling": mt5.ORDER_FILLING_IOC,
            "tp": price + take_profit * symbol_info.point if order_type == mt5.ORDER_TYPE_BUY else price - take_profit * symbol_info.point,
            "sl": price - stop_loss * symbol_info.point if order_type == mt5.ORDER_TYPE_BUY else price + stop_loss * symbol_info.point,
        }
        result = mt5.order_send(request)
        if result is not None and result.retcode == mt5.TRADE_RETCODE_DONE:
            print(f"Test limit order placed for {symbol}")
            return result.order
        else:
            print(f"Error: Test limit order not placed for {symbol}, retcode={result.retcode if result is not None else 'None'}")
            return None
    else:
        print(f"Error: Symbol info not found for {symbol}")
        return None

我们的函数做的第一件事是尝试连接到 MetaTrader 5 终端。然后它会检查我们想要交易的工具是否存在。

以下代码:

if positions_total >= MAX_OPEN_TRADES:
    print("MAX POSITIONS TOTAL!")
    return None

此项检查可确保我们不会开启过多的仓位。

现在下一步是生成开启订单的请求。这里的参数相当多。订单类型、交易量、价格、偏差、幻数、注释……如果一切顺利,该函数会告诉我们。如果没有,则会出现消息。

这就是 open_test_limit_order 函数的工作原理。这是我们与市场的联系,从某种程度上来说,它发挥着经纪商的作用。


临时交易限制:在特定时间段内工作

现在我们来谈谈交易时间。 

if current_time >= datetime.strptime("23:30", "%H:%M").time() or current_time <= datetime.strptime("05:00", "%H:%M").time():
    print("Current time is between 23:30 and 05:00. Skipping execution.")
    time.sleep(300)  # Wait for 5 minutes before checking again
    continue

这里发生了什么?我们的系统会检查时间。如果时钟显示的时间介于晚上 11:30 至凌晨 5:00 之间,则系统认为这不是交易时间,并进入等待模式 5 分钟。然后它会激活,再次检查时间,如果还早,就会再次进入等待模式。

我们为什么需要这样做?这是有原因的。第一,流动性,晚上通常较少。第二,库存费,到了晚上,它们就会增长。第三,新闻,最重要的消息通常会在工作时间发布。


运行时循环和错误处理

让我们看一下 “main” 函数。它就像一位船长,但是没有方向盘,而是有一个键盘。它起什么作用?一切都很简单:

  1. 收集数据
  2. 计算合成价格 
  3. 寻找套利机会 
  4. 开启订单

还有一些错误处理。 

def main():
    data = get_currency_data()
    synthetic_prices = calculate_synthetic_prices(data)
    method_count = 2000  # Define the method_count variable here
    arbitrage_opportunities = analyze_arbitrage(data, synthetic_prices, method_count)

    # Trade based on arbitrage opportunities
    for symbol in arbitrage_opportunities.columns:
        if arbitrage_opportunities[symbol].any():
            direction = "BUY" if arbitrage_opportunities[symbol].iloc[0] else "SELL"
            symbol = symbol.split('_')[0]  # Remove the index from the symbol
            symbol_info = mt5.symbol_info_tick(symbol)
            if symbol_info is not None:
                price = symbol_info.bid if direction == "BUY" else symbol_info.ask
                take_profit = 450
                stop_loss = 200
                order = open_test_limit_order(symbol, mt5.ORDER_TYPE_BUY if direction == "BUY" else mt5.ORDER_TYPE_SELL, price, 0.50, take_profit, stop_loss, terminal_path)
            else:
                print(f"Error: Symbol info tick not found for {symbol}")


系统可扩展性:添加新的货币对和方法

您想添加新的货币对吗?只需将其添加到此列表中:

symbols = ["EURUSD", "GBPUSD", "USDJPY", ... , "YOURPAIR"]

系统现在知道了新的货币对。 。新的计算方法怎么样? 

def calculate_synthetic_prices(data):
    # ... existing code ...
    
    # Add a new method
    synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['ask'] / data[pair2]['bid']
    method_count += 1


套利系统的测试和回测

让我们来谈谈回溯测试。这对任何交易系统来说都是一个非常重要的问题。我们的套利系统也不例外。

我们做了些什么?我们通过历史数据来执行我们的策略。为什么呢?了解它的效率如何。我们的代码从 get_historical_data 开始。此函数从 MetaTrader 5 获取旧数据。如果没有这些数据,我们就无法高效地工作。

然后是 calculate_synthetic_prices。这里我们计算合成汇率。这是我们套利策略的关键部分。Analyze_arbitrage 是我们的机会探测器。它将真实价格与合成价格进行比较并找出差异,这样我们就可以获得潜在的利润。simulate_trade 几乎是一个交易过程。然而,它发生在测试模式下。这是一个非常重要的过程:在模拟中犯错比损失真钱要好。

最后,backtest_arbitrage_system 将所有内容整合在一起,并通过历史数据运行我们的策略。日复一日,一桩交易又一桩交易。

import MetaTrader5 as mt5
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
import pytz

# Path to MetaTrader 5 terminal
terminal_path = "C:/Program Files/ForexBroker - MetaTrader 5/Arima/terminal64.exe"

def remove_duplicate_indices(df):
    """Removes duplicate indices, keeping only the first row with a unique index."""
    return df[~df.index.duplicated(keep='first')]

def get_historical_data(start_date, end_date, terminal_path):
    if not mt5.initialize(path=terminal_path):
        print(f"Failed to connect to MetaTrader 5 terminal at {terminal_path}")
        return None

    symbols = ["AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"]
    
    historical_data = {}
    for symbol in symbols:
        timeframe = mt5.TIMEFRAME_M1
        rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date)
        if rates is not None and len(rates) > 0:
            df = pd.DataFrame(rates)
            df['time'] = pd.to_datetime(df['time'], unit='s')
            df.set_index('time', inplace=True)
            df = df[['open', 'high', 'low', 'close']]
            df['bid'] = df['close']  # Simplification: use 'close' as 'bid'
            df['ask'] = df['close'] + 0.000001  # Simplification: add spread
            historical_data[symbol] = df

    mt5.shutdown()
    return historical_data

def calculate_synthetic_prices(data):
    synthetic_prices = {}
    pairs = [('AUDUSD', 'USDCHF'), ('AUDUSD', 'NZDUSD'), ('AUDUSD', 'USDJPY'),
             ('USDCHF', 'USDCAD'), ('USDCHF', 'NZDCHF'), ('USDCHF', 'CHFJPY'),
             ('USDJPY', 'USDCAD'), ('USDJPY', 'NZDJPY'), ('USDJPY', 'GBPJPY'),
             ('NZDUSD', 'NZDCAD'), ('NZDUSD', 'NZDCHF'), ('NZDUSD', 'NZDJPY'),
             ('GBPUSD', 'GBPCAD'), ('GBPUSD', 'GBPCHF'), ('GBPUSD', 'GBPJPY'),
             ('EURUSD', 'EURCAD'), ('EURUSD', 'EURCHF'), ('EURUSD', 'EURJPY'),
             ('CADCHF', 'CADJPY'), ('CADCHF', 'GBPCAD'), ('CADCHF', 'EURCAD'),
             ('CHFJPY', 'GBPCHF'), ('CHFJPY', 'EURCHF'), ('CHFJPY', 'NZDCHF'),
             ('NZDCAD', 'NZDJPY'), ('NZDCAD', 'GBPNZD'), ('NZDCAD', 'EURNZD'),
             ('NZDCHF', 'NZDJPY'), ('NZDCHF', 'GBPNZD'), ('NZDCHF', 'EURNZD'),
             ('NZDJPY', 'GBPNZD'), ('NZDJPY', 'EURNZD')]

    for pair1, pair2 in pairs:
        if pair1 in data and pair2 in data:
            synthetic_prices[f'{pair1}_{pair2}_1'] = data[pair1]['bid'] / data[pair2]['ask']
            synthetic_prices[f'{pair1}_{pair2}_2'] = data[pair1]['bid'] / data[pair2]['bid']

    return pd.DataFrame(synthetic_prices)

def analyze_arbitrage(data, synthetic_prices):
    spreads = {}
    for pair in data.keys():
        for synth_pair in synthetic_prices.columns:
            if pair in synth_pair:
                spreads[synth_pair] = data[pair]['bid'] - synthetic_prices[synth_pair]

    arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008
    return arbitrage_opportunities

def simulate_trade(data, direction, entry_price, take_profit, stop_loss):
    for i, row in data.iterrows():
        current_price = row['bid'] if direction == "BUY" else row['ask']
        
        if direction == "BUY":
            if current_price >= entry_price + take_profit:
                return {'profit': take_profit * 800, 'duration': i}
            elif current_price <= entry_price - stop_loss:
                return {'profit': -stop_loss * 400, 'duration': i}
        else:  # SELL
            if current_price <= entry_price - take_profit:
                return {'profit': take_profit * 800, 'duration': i}
            elif current_price >= entry_price + stop_loss:
                return {'profit': -stop_loss * 400, 'duration': i}
    
    # If the loop completes without hitting TP or SL, close at the last price
    last_price = data['bid'].iloc[-1] if direction == "BUY" else data['ask'].iloc[-1]
    profit = (last_price - entry_price) * 100000 if direction == "BUY" else (entry_price - last_price) * 100000
    return {'profit': profit, 'duration': len(data)}

def backtest_arbitrage_system(historical_data, start_date, end_date):
    equity_curve = [10000]  # Starting with $10,000
    trades = []
    dates = pd.date_range(start=start_date, end=end_date, freq='D')

    for current_date in dates:
        print(f"Backtesting for date: {current_date.date()}")
        
        # Get data for the current day
        data = {symbol: df[df.index.date == current_date.date()] for symbol, df in historical_data.items()}
        
        # Skip if no data for the current day
        if all(df.empty for df in data.values()):
            continue

        synthetic_prices = calculate_synthetic_prices(data)
        arbitrage_opportunities = analyze_arbitrage(data, synthetic_prices)

        # Simulate trades based on arbitrage opportunities
        for symbol in arbitrage_opportunities.columns:
            if arbitrage_opportunities[symbol].any():
                direction = "BUY" if arbitrage_opportunities[symbol].iloc[0] else "SELL"
                base_symbol = symbol.split('_')[0]
                if base_symbol in data and not data[base_symbol].empty:
                    price = data[base_symbol]['bid'].iloc[-1] if direction == "BUY" else data[base_symbol]['ask'].iloc[-1]
                    take_profit = 800 * 0.00001  # Convert to price
                    stop_loss = 400 * 0.00001  # Convert to price
                    
                    # Simulate trade
                    trade_result = simulate_trade(data[base_symbol], direction, price, take_profit, stop_loss)
                    trades.append(trade_result)
                    
                    # Update equity curve
                    equity_curve.append(equity_curve[-1] + trade_result['profit'])

    return equity_curve, trades

def main():
    start_date = datetime(2024, 1, 1, tzinfo=pytz.UTC)
    end_date = datetime(2024, 8, 31, tzinfo=pytz.UTC)  # Backtest for January-August 2024
    
    print("Fetching historical data...")
    historical_data = get_historical_data(start_date, end_date, terminal_path)
    
    if historical_data is None:
        print("Failed to fetch historical data. Exiting.")
        return

    print("Starting backtest...")
    equity_curve, trades = backtest_arbitrage_system(historical_data, start_date, end_date)

    total_profit = sum(trade['profit'] for trade in trades)
    win_rate = sum(1 for trade in trades if trade['profit'] > 0) / len(trades) if trades else 0

    print(f"Backtest completed. Results:")
    print(f"Total Profit: ${total_profit:.2f}")
    print(f"Win Rate: {win_rate:.2%}")
    print(f"Final Equity: ${equity_curve[-1]:.2f}")

    # Plot equity curve
    plt.figure(figsize=(15, 10))
    plt.plot(equity_curve)
    plt.title('Equity Curve: Backtest Results')
    plt.xlabel('Trade Number')
    plt.ylabel('Account Balance ($)')
    plt.savefig('equity_curve.png')
    plt.close()

    print("Equity curve saved as 'equity_curve.png'.")

if __name__ == "__main__":
    main()

为什么这很重要?因为回溯测试显示了我们的系统效率如何。它能盈利吗?还是会耗尽你的存款?回撤怎么样?交易获胜的百分比是多少?我们从回测中了解到了这一切。

当然,过去的结果并不能保证未来的结果,市场正在发生变化。但如果没有回测,我们就不会得到任何结果。知道了结果,我们大概就知道会发生什么。另一个重点 —— 回溯测试有助于优化系统。我们改变参数并反复查看结果。因此,我们一步步地完善我们的系统。

以下是我们的系统回测结果:

以下是在 MetaTrader 5 中对系统的测试:

以下是该系统的 MQL5 EA 代码:

//+------------------------------------------------------------------+
//|                                                 TrissBotDemo.mq5 |
//|                                  Copyright 2024, MetaQuotes Ltd. |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, MetaQuotes Ltd."
#property link      "https://www.mql5.com"
#property version   "1.00"
// Input parameters
input int MAX_OPEN_TRADES = 10;
input double VOLUME = 0.50;
input int TAKE_PROFIT = 450;
input int STOP_LOSS = 200;
input double MIN_SPREAD = 0.00008;

// Global variables
string symbols[] = {"AUDUSD", "AUDJPY", "CADJPY", "AUDCHF", "AUDNZD", "USDCAD", "USDCHF", "USDJPY", "NZDUSD", "GBPUSD", "EURUSD", "CADCHF", "CHFJPY", "NZDCAD", "NZDCHF", "NZDJPY", "GBPCAD", "GBPCHF", "GBPJPY", "GBPNZD", "EURCAD", "EURCHF", "EURGBP", "EURJPY", "EURNZD"};
int symbolsTotal;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    symbolsTotal = ArraySize(symbols);
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    // Cleanup code here
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    if(!IsTradeAllowed()) return;
    
    datetime currentTime = TimeGMT();
    if(currentTime >= StringToTime("23:30:00") || currentTime <= StringToTime("05:00:00"))
    {
        Print("Current time is between 23:30 and 05:00. Skipping execution.");
        return;
    }
    
    AnalyzeAndTrade();
}

//+------------------------------------------------------------------+
//| Analyze arbitrage opportunities and trade                        |
//+------------------------------------------------------------------+
void AnalyzeAndTrade()
{
    double synthetic_prices[];
    ArrayResize(synthetic_prices, symbolsTotal);
    
    for(int i = 0; i < symbolsTotal; i++)
    {
        synthetic_prices[i] = CalculateSyntheticPrice(symbols[i]);
        double currentPrice = SymbolInfoDouble(symbols[i], SYMBOL_BID);
        
        if(MathAbs(currentPrice - synthetic_prices[i]) > MIN_SPREAD)
        {
            if(currentPrice > synthetic_prices[i])
            {
                OpenOrder(symbols[i], ORDER_TYPE_SELL);
            }
            else
            {
                OpenOrder(symbols[i], ORDER_TYPE_BUY);
            }
        }
        
    }
}

//+------------------------------------------------------------------+
//| Calculate synthetic price for a symbol                           |
//+------------------------------------------------------------------+
double CalculateSyntheticPrice(string symbol)
{
    // This is a simplified version. You need to implement the logic
    // to calculate synthetic prices based on your specific method
    return SymbolInfoDouble(symbol, SYMBOL_ASK);
}

//+------------------------------------------------------------------+
//| Open a new order                                                 |
//+------------------------------------------------------------------+
void OpenOrder(string symbol, ENUM_ORDER_TYPE orderType)
{
    if(PositionsTotal() >= MAX_OPEN_TRADES)
    {
        Print("MAX POSITIONS TOTAL!");
        return;
    }
    
    double price = (orderType == ORDER_TYPE_BUY) ? SymbolInfoDouble(symbol, SYMBOL_ASK) : SymbolInfoDouble(symbol, SYMBOL_BID);
    double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
    
    double tp = (orderType == ORDER_TYPE_BUY) ? price + TAKE_PROFIT * point : price - TAKE_PROFIT * point;
    double sl = (orderType == ORDER_TYPE_BUY) ? price - STOP_LOSS * point : price + STOP_LOSS * point;
    
    MqlTradeRequest request = {};
    MqlTradeResult result = {};
    
    request.action = TRADE_ACTION_DEAL;
    request.symbol = symbol;
    request.volume = VOLUME;
    request.type = orderType;
    request.price = price;
    request.deviation = 30;
    request.magic = 123456;
    request.comment = "ArbitrageAdvisor";
    request.type_time = ORDER_TIME_GTC;
    request.type_filling = ORDER_FILLING_IOC;
    request.tp = tp;
    request.sl = sl;
    
    if(!OrderSend(request, result))
    {
        Print("OrderSend error ", GetLastError());
        return;
    }
    
    if(result.retcode == TRADE_RETCODE_DONE)
    {
        Print("Order placed successfully");
    }
    else
    {
        Print("Order failed with retcode ", result.retcode);
    }
}

//+------------------------------------------------------------------+
//| Check if trading is allowed                                      |
//+------------------------------------------------------------------+
bool IsTradeAllowed()
{
    if(!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED))
    {
        Print("Trade is not allowed in the terminal");
        return false;
    }
    
    if(!MQLInfoInteger(MQL_TRADE_ALLOWED))
    {
        Print("Trade is not allowed in the Expert Advisor");
        return false;
    }
    
    return true;
}


经纪人系统的可能改进和合法性,或如何避免用限价单打击流动性提供者

我们的系统还有其他潜在的困难。经纪商和流动性提供商通常不赞成这样的系统。为什么呢?因为我们本质上是从市场中获取必要的流动性。他们甚至为此想出了一个专门的术语 —— 有毒订单流。 

这是一个真正的问题。我们通过市场订单从系统中吸收流动性。每个人都需要它:无论是大型参与者还是小型交易者。当然,这有其后果。

在这种情况下该怎么办?有一个折衷方案 —— 限价订单。 

但这并不能解决所有问题:有毒订单流标签的贴出并不是因为吸收了市场当前的流动性,而是因为服务这种订单流的负荷很高。我还没有解决这个问题。例如,花费 100 美元来服务大量套利交易,并从中获得 50 美元的佣金,这是无利可图的。因此,这里的关键可能是高周转率和高手数大小,以及高周转速度。那么经纪商可能也愿意支付回扣。

现在我们开始讨论代码。我们怎样才能改进它?首先,我们可以添加一个处理限价订单的函数。这里还有很多工作要做 —— 我们需要仔细考虑等待和取消未执行订单的逻辑。

机器学习可能是改进系统的一个有趣的想法。我认为可以训练我们的系统来预测哪些套利机会最有可能奏效。 


结论

让我们总结一下。我们创建了一个寻找套利机会的系统。 请记住,系统并不能解决你所有的财务问题。 

我们已经完成了回溯测试。它使用基于时间的数据,甚至更好的是,它让我们看到我们的系统在过去是如何工作的。但请记住 —— 过去的结果并不能保证未来的结果。市场是一个不断变化的复杂机制。

但你知道什么是最重要的吗?不是代码,不是算法,而是您。您渴望学习、尝试、犯错并再次尝试。这确实是无价的。

所以不要就此止步,该系统只是您在算法交易世界中旅程的开始。将其作为新想法和新策略的起点。就像生活中一样,交易中最重要的是平衡。风险与谨慎、贪婪与理性、复杂与简单之间的平衡。

祝你在这段激动人心的旅程中好运,愿你的算法永远领先市场一步!

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

最近评论 | 前往讨论 (8)
Andrey Khatimlianskii
Andrey Khatimlianskii | 28 10月 2024 在 16:09
pivomoe #:

第一对的出价是多少?第一对是

AUDUSD 也是一对。澳元兑美元。

Roman Shiredchenko
Roman Shiredchenko | 28 10月 2024 在 20:12
pivomoe #:

请解释一下这是怎么回事:

这里有一对:

第一对的出价是多少?第一对是:

这就是人工合成的方式。不是通过差异,而是通过分割。不是简单的--而是...阅读.....
leonerd
leonerd | 21 11月 2024 在 10:59
ticks = mt5.copy_ticks_from(symbol, utc_from, count, mt5.COPY_TICKS_ALL)

全部安装完毕。这是用"√"表示的内容:

array([b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b'', b''、

...

B'',B'',B'',B'',B'',B'',B'',B'',B'',B'',B'',B'',B''、

B'',B'',B'',B'',B'',B'',B'',B'',B'',B'',B''、

b''、b''、b''、b''、b''、b''、b''、b''、b'']、

dtype='|V0')


在这里,我们已经按时得到了一个出口:

ticks_frame['time'] = pd.to_datetime(ticks_frame['time'], unit='s')
leonerd
leonerd | 22 11月 2024 在 09:11

https://www.mql5.com/zh/docs/python_metatrader5/mt5copyticksfrom_py 示例中的代码也不起作用

>>>  timezone = pytz.timezone("Etc/UTC")
>>>  utc_from = datetime(2020, 1, 10, tzinfo=timezone)
>>>  ticks = mt5.copy_ticks_from("EURUSD", utc_from, 100000, mt5.COPY_TICKS_ALL)
>>>
>>> print("收到蜱虫:",len(ticks))
Получено тиков: 100000
>>> print("让我们来看看结果如何吧")
Выведем полученные тики как есть
>>>  count = 0
>>> for tick in ticks:
...     count+=1
...     print(tick)
...     if count >= 100:
...         break
...
b''
b''
b''
b''

总之,Python 是什么样的语言?如何准备?不清楚...

Luxus_67
Luxus_67 | 24 2月 2026 在 19:08
好文章。谢谢。
MQL5 中的 SQLite 功能示例:按交易品种及 Magic 编码展示交易统计信息的仪表盘 MQL5 中的 SQLite 功能示例:按交易品种及 Magic 编码展示交易统计信息的仪表盘
本文将介绍如何创建一个指标型仪表盘,按账户、交易品种及交易策略展示交易统计信息。我们将以官方文档及数据库相关文章中的示例为基础,逐步实现完整程序。
您应当知道的 MQL5 向导技术(第 44 部分):平均真实范围(ATR)技术指标 您应当知道的 MQL5 向导技术(第 44 部分):平均真实范围(ATR)技术指标
ATR 振荡指标是一款非常流行的指标,权当波动率代表,尤其是在交易量数据稀缺的外汇市场当中。我们以形态为基础来验证这一点,就如我们对先前指标所做那样,并分享策略和测试报告,致谢 MQL5 向导库的类和汇编。
使用Python与MQL5进行多个交易品种分析(第二部分):主成分分析在投资组合优化中的应用 使用Python与MQL5进行多个交易品种分析(第二部分):主成分分析在投资组合优化中的应用
交易账户风险管理是所有交易者面临的共同挑战。我们如何在MetaTrader 5中开发能够动态学习不同交易品种的高、中、低风险模式的交易应用?通过主成分分析(PCA),我们可以更有效地控制投资组合的方差。本文将演示如何从MetaTrader 5获取的市场数据中,训练出这三种风险模式的交易模型。
构建K线趋势约束模型(第九部分):多策略EA(2) 构建K线趋势约束模型(第九部分):多策略EA(2)
理论上,可以集成至EA中的策略数量没有上限。然而,每新增一种策略都会提升算法复杂度。通过融合多策略架构,EA能够更灵活地适应不同市场环境,从而可能提升整体盈利能力。今天,我们将探讨如何通过MQL5实现理查德·唐奇安(Richard Donchian)的经典通道突破策略,以此进一步拓展我们的趋势约束型EA功能体系。