
Высокочастотная арбитражная торговая система на Python с использованием MetaTrader 5
Введение
Валютный рынок. Алгоритмические стратегии. Python и MetaTrader 5. Это было объединено в единое целое, когда я начал работу над арбитражной торговой системой. Задумка была простой — создать высокочастотную систему для поиска ценовых дисбалансов. К чему это все привело в итоге?
API MetaTrader 5 я использовал чаще всего в этот период. Начал с такой идеи — решил посчитать синтетические кросс-курсы. Я решил не ограничиваться десятью или сотней. Количество перешло границы тысячи.
Отдельно стояла задача управления рисками. В этой статье я расскажу обо всём. Архитектура системы, алгоритмы, принятие решений — мы разберем все. Покажу результаты бэктестинга и живой торговли. И конечно, поделюсь идеями на будущее. Кто знает, может, кто-то из вас захочет развить эту тему дальше? Я надеюсь, что моя работа окажется востребованной. Хочется верить, что она внесёт свой вклад в развитие алгоритмической торговли. Может, кто-то возьмёт её за основу и создаст что-то ещё более эффективное в мире высокочастотного арбитража. В конце концов, в этом и есть суть науки — двигаться вперёд, опираясь на опыт предшественников. Давайте, перейдем непосредственно к сути.
Введение в арбитражную торговлю на Форекс
Арбитраж на Форекс. Давайте разберёмся, что это такое на самом деле.
Можно провести аналогию с обменом валют. Допустим, что вы можете купить доллары за евро в одном месте, тут же продать их за фунты в другом, а потом обменять фунты обратно на евро и в итоге оказаться в плюсе. Это и есть арбитраж в его простейшей форме.
На самом деле, всё немного сложнее. Форекс — это огромный, децентрализованный рынок. Тут большое количество банков, брокеров, фондов. И у каждого свои курсы. Чаще всего, эти курсы не совпадают. Тут у нас и появляется возможность для арбитража. Но не думайте, что это легкие деньги. Обычно эти несоответствия в ценах живут считанные секунды. Или даже миллисекунды. Успеть практически невозможно. Тут нужны мощные компьютеры и быстрые алгоритмы.
А ещё есть разные виды арбитража. Простой — это когда мы играем на разнице курсов в разных местах. Сложный — когда мы используем кросс-курсы. Например, считаем, сколько будет стоить фунт через доллар и евро, и сравниваем с прямым курсом фунт/евро.
На этом список не заканчивается. Есть ещё временной арбитраж. Тут мы играем на разнице цен в разные моменты времени. Купил сейчас, продал через минуту. Конечно, процесс кажется простым. Но главная проблема заключается в том, что неизвестно, куда пойдёт цена через минуту. В этом и заключаются основные риски. Рынок может развернуться быстрее, чем вы успеете активировать нужный ордер. Или ваш брокер может задержать с исполнением ордеров. В общем, сложностей и рисков довольно много. Несмотря на все сложности, арбитраж на Форекс — это довольно востребованная система. Тут задействованы серьезные денежные ресурсы и достаточно трейдеров, специализирующихся только на этом.
Теперь, после небольшого введения в курс дела, приступим непосредственно к нашей работе со стратегией.Обзор используемых технологий: Python и MetaTrader 5
Итак, Python и MetaTrader 5.
Python — многофункциональный, простой в понимании. Не зря его предпочитают и начинающие кодеры, и опытные разработчики. А для анализа данных он подходит лучше всего.
С другой стороны, MetaTrader 5. Платформа, знакомая каждому форекс-трейдеру. Надёжная и также не сложная. И тоже довольно функциональная — котировки в реальном времени, и торговые роботы, и технический анализ. Всё в одном приложении. Для достижения положительных результатов нам все это необходимо объединить.
Что происходит: Python берёт данные из MetaTrader 5, обрабатывает их с помощью своих библиотек, а потом отправляет команды обратно в MetaTrader 5 для исполнения сделок. Конечно, есть свои трудности. Но вместе эти приложения очень продуктивны.
Для работы с MetaTrader 5 из Python есть специальная библиотека от разработчиков. Для активации нужно просто установить ее. Получаем котировки, отправляем ордера, управляем позициями. Всё как в самом терминале, только теперь еще задействованы возможности Python.
Какие функции и возможности нам теперь доступны? Их теперь довольно много. Например, автоматизировать торговлю, провести сложный анализ исторических данных. Мы можем даже создать свою собственную торговую платформу. Однако это уже задача для продвинутых пользователей, но тоже возможно.
Настройка окружения: установка необходимых библиотек и подключение к MetaTrader 5
Начнем мы рабочий процесс с Python. Если он у вас еще не установлен, тогда заходим на python.org. Скачивайте, устанавливайте. Также необходимо установить согласие на "ADD TO PATCH".
Наш следующий шаг — библиотеки. Нам понадобится несколько. Главная из них — MetaTrader5. Установка не требует особых навыков.
Открываем командную строку и пишем:
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("Увы и ах, не получилось подключиться :(") mt5.shutdown() else: print("Ура! Мы подключились!")
Если все запустилось, хорошо — значит, можно приступать к следующей части.
Структура нашего кода: основные функции и их назначение
Начнем с 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_mt5_data.
Работа с множественными валютными парами: функция 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), наша функция берет только самое важное: время, bid и ask.
И вот, наконец, grand finale. Функция возвращает наполненный данными словарь.
Теперь получаем 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
Что происходит далее? Сначала мы создаем пустой словарь synthetic_prices. Мы также наполним его данными. Затем мы проходимся по всем данным и удаляем дубликаты индексов, чтобы избежать ошибок в дальнейшем.
А теперь следующий шаг — список pairs. Это наши валютные пары, которые мы будем использовать для синтеза. Дальше начинается еще один процесс. Мы запускаем цикл по всем парам. Для каждой пары мы рассчитываем синтетическую цену двумя способами:
- Делим bid первой пары на ask второй.
- Делим bid первой пары на bid второй.
И каждый раз мы увеличиваем наш method_count. В итоге у нас получается не 1000, не 1500, а целых 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("Арбитражные возможности:") 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. Мы также наполним его данными.
Переходим к следующему шагу. Функция проходится по всем валютным парам и их синтетическим аналогам. Для каждой пары она вычисляет спред — разницу между реальной ценой bid и синтетической ценой.
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
Что тут происходит? Наша система проверяет время. Если часы показывают от 23: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): # ... существующий код ... # Добавляем новый метод 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()
Почему это важно? Потому что бэктестинг показывает, насколько эффективна наша система. Прибыльна ли она, или сливает депозит? Какая просадка? Какой процент выигрышных сделок? Всё это мы узнаем именно из бэктеста.
Конечно, прошлые результаты не гарантируют будущих. Рынок меняется. Но без бэктеста мы не получим никаких результатов. А с ним — мы ориентировочно знаем, чего ожидать. И ещё важный момент: бэктестинг помогает оптимизировать систему. Меняем параметры, смотрим результат. Опять меняем, опять смотрим. Так, шаг за шагом, мы делаем нашу систему лучше.
Собственно, вот результат нашей работы системы на бэктесте:
Вот тест системы в MetaТrader 5:
А вот и код советника MQL5 по данной системе:
//+------------------------------------------------------------------+ //| 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; }
Пути улучшения и легальность системы для брокеров, или как не нанести удар по поставщику ликвидности с помощью лимитных ордеров
Однако у нашей системы есть и другие трудности. Брокеры и поставщики ликвидности часто относятся к таким системам неодобрительно. Почему? Да потому что мы, по сути, забираем необходимую ликвидность из рынка. Они даже придумали для этого специальный термин — "Toxic Order Flow".
Это реальная проблема. Мы своими рыночными ордерами буквально поглощаем ликвидность из системы. А она нужна всем: и крупным игрокам, и мелким трейдерам. Конечно это влечет свои последствия.
Как поступить в данной ситуации? Есть компромисс — лимитные ордера.
Но на этом все проблемы не решены: метку Toxic Order Flow ставят не столько из-за поглощения ликвидности из рынка в моменте, сколько из-за высоких нагрузок на обслуживание такого потока ордеров. Эту проблему я пока не решил. К примеру, тратить 100 долларов (возьмем цифру теоретически) на обслуживание огромного потока сделок арбитражника, получая с него комиссии допустим, 50 — это невыгодно. Так что возможно, здесь ключ — в больших оборотах и высоких размерах лотов, а также высокой скорости их оборота. Тогда брокеры еще и ребейты будут готовы платить?
Теперь о коде. Как его улучшить? Во-первых, можно добавить функцию для работы с лимитными ордерами. Здесь тоже массив работы — нужно продумать логику ожидания, отмены неисполненных ордеров.
Интересная мысль по улучшению системы — машинное обучение. Я предполагаю, что возможно научить нашу систему предсказывать, какие арбитражные возможности вероятнее всего сработают.
Заключение
Давайте подведем итоги. Мы создали систему, которая ищет арбитражные возможности. Помните, наша система — это не решение всех ваших финансовых проблем.
Мы разобрались с бэктестингом. Это работа с временными данными, и даже лучше — позволяет увидеть, как бы работала наша система в прошлом. Но не забывайте: прошлые результаты не гарантируют будущих. Рынок — сложный механизм, постоянно меняется.
Но знаете, что самое важное? Не код. Не алгоритмы. А вы сами. Ваше желание учиться, экспериментировать, ошибаться и снова пробовать. Вот это действительно бесценно.
Так что не останавливайтесь на достигнутом. Эта система — лишь начало вашего пути в мире алготрейдинга. Используйте ее как отправную точку для новых идей, новых стратегий. И помните: в трейдинге, как в жизни, главное — баланс. Между риском и осторожностью, между жадностью и разумностью, между сложностью и простотой.
Удачи вам на этом увлекательном пути, и пусть ваши алгоритмы всегда будут на шаг впереди рынка!





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Объясните пожалуйста, о чем это:
Вот пары:
Что такое Bid первой пары ? Первая пара это ведь:
Что такое Bid первой пары ? Первая пара это ведь:
AUDUSD - тоже пара. AUD к USD.
Объясните пожалуйста, о чем это:
Вот пары:
Что такое Bid первой пары ? Первая пара это ведь:
ticks = mt5.copy_ticks_from(symbol, utc_from, count, mt5.COPY_TICKS_ALL)
Всё установил. Вот что приходит в ticks:
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'', b'', b'', b'', b''],
dtype='|V0')
А здесь уже вылазит эксепшен на time:
Не работает и код из примера https://www.mql5.com/ru/docs/python_metatrader5/mt5copyticksfrom_py
В общем, какой питон? Как это готовить? Непонятно...