English Русский Español Deutsch 日本語 Português
preview
使用 MetaTrader 5 的 Python 高频套利交易系统

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

MetaTrader 5交易 |
66 7
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

最近评论 | 前往讨论 (7)
pivomoe
pivomoe | 24 10月 2024 在 00:31

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

А теперь следующий шаг — список pairs. Это наши валютные пары, которые мы будем использовать для синтеза. Дальше начинается еще один процесс. Мы запускаем цикл по всем парам. Для каждой пары мы рассчитываем синтетическую цену двумя способами:

Делим bid первой пары на ask второй.
Делим bid первой пары на bid второй.
И каждый раз мы увеличиваем наш method_count. В итоге у нас получается не 1000, не 1500, а целых 2000 синтетических цен!

这里有一对:

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')]

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

('AUDUSD', 'USDCHF')
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 是什么样的语言?如何准备?不清楚...

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功能体系。