
使用 MetaTrader 5 的 Python 高频套利交易系统
概述
外汇市场,算法策略,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” 列表,这些是我们将用于合成的货币对。然后另一个过程开始,我们对全部对进行循环。对于每一货币对,我们通过两种方式计算合成价格:
- 将第一对的买入价除以第二对的卖出价。
- 将第一对的卖出价除以第二对的买入价。
每次我们都增加我们的 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” 函数。它就像一位船长,但是没有方向盘,而是有一个键盘。它起什么作用?一切都很简单:
- 收集数据
- 计算合成价格
- 寻找套利机会
- 开启订单
还有一些错误处理。
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




请解释一下这是怎么回事:
这里有一对:
第一对的出价是多少?第一对是:
第一对的出价是多少?第一对是
AUDUSD 也是一对。澳元兑美元。
请解释一下这是怎么回事:
这里有一对:
第一对的出价是多少?第一对是:
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')
在这里,我们已经按时得到了一个出口:
https://www.mql5.com/zh/docs/python_metatrader5/mt5copyticksfrom_py 示例中的代码也不起作用
总之,Python 是什么样的语言?如何准备?不清楚...