English Русский Español Deutsch 日本語
preview
Sistema de negociação de arbitragem de alta frequência em Python usando MetaTrader 5

Sistema de negociação de arbitragem de alta frequência em Python usando MetaTrader 5

MetaTrader 5Negociação | 3 abril 2025, 13:36
385 0
Yevgeniy Koshtenko
Yevgeniy Koshtenko

Introdução

O mercado cambial. Estratégias algorítmicas. Python e MetaTrader 5. Tudo isso foi combinado em um único sistema de arbitragem quando comecei a trabalhar nele. A ideia era simples: criar um sistema de alta frequência para detectar desequilíbrios de preços. E no que isso se transformou?

Durante esse período, o API do MetaTrader 5 foi a ferramenta que mais utilizei. Comecei com a seguinte ideia: calcular cotações cruzadas sintéticas. Decidi não me limitar a dez ou cem. O número ultrapassou a casa dos mil.

Havia também a tarefa separada de gerenciamento de risco. Neste artigo, vou explicar tudo. Vamos analisar todos os aspectos do sistema: sua arquitetura, os algoritmos utilizados e o processo de tomada de decisões. Mostrarei os resultados do backtest e da negociação ao vivo. E, claro, compartilharei ideias para o futuro. Quem sabe alguém de vocês queira continuar desenvolvendo esse tema? Espero que meu trabalho seja útil. Quero acreditar que ele contribuirá para o avanço da negociação algorítmica. Talvez alguém o use como base para criar algo ainda mais eficiente no mundo da arbitragem de alta frequência. Afinal, esse é o espírito da ciência: avançar com base na experiência daqueles que vieram antes. Vamos direto ao ponto.


Introdução à negociação de arbitragem no Forex

Arbitragem no Forex. Vamos entender o que isso realmente significa.

Podemos fazer uma analogia com a troca de moedas. Suponha que você possa comprar dólares com euros em um lugar e vendê-los imediatamente por libras em outro, depois trocar as libras de volta por euros, obtendo lucro. Isso é arbitragem em sua forma mais simples.

Na prática, porém, é um pouco mais complicado. O mercado de câmbio (Forex) é enorme e descentralizado. Envolve um grande número de bancos, corretoras e fundos. E cada um tem suas próprias cotações. Geralmente, essas cotações não coincidem. É aí que surge a oportunidade de arbitragem. No entanto, não pense que é dinheiro fácil. Geralmente, essas discrepâncias de preços duram apenas alguns segundos. Às vezes, até mesmo milissegundos. Aproveitá-las é quase impossível. É necessário ter computadores potentes e algoritmos rápidos.

Além disso, há diferentes tipos de arbitragem. A simples, quando lucramos com a diferença de preços em diferentes lugares. A arbitragem complexa ocorre quando usamos cotações cruzadas. Por exemplo, calculamos o valor da libra considerando sua passagem pelo dólar e pelo euro e o comparamos com a cotação direta libra/euro.

A lista não para por aí. Há também a arbitragem temporal. Nela, operamos com base na diferença de preços em momentos diferentes no tempo. Compramos agora e vendemos daqui a um minuto. Embora o processo pareça simples, No entanto, o principal problema é que não sabemos para onde o preço vai em um minuto. E é justamente aí que estão os maiores riscos. O mercado pode se mover na direção contrária antes que você consiga ativar a ordem desejada. Ou sua corretora pode atrasar a execução dos pedidos. Enfim, há muitos riscos e dificuldades. Apesar de tudo isso, a arbitragem no Forex é um sistema bastante procurado. Ela envolve recursos financeiros consideráveis, e há traders que se especializam exclusivamente nisso.

Agora, depois dessa breve introdução ao tema, vamos direto ao nosso trabalho com a estratégia.


Visão geral das tecnologias utilizadas: Python e MetaTrader 5

Então, Python e MetaTrader 5. 

Python é multifuncional e fácil de entender. Não é à toa que é a linguagem preferida tanto de iniciantes quanto de desenvolvedores experientes. É também uma das melhores opções para análise de dados.

Por outro lado, temos o MetaTrader 5. Plataforma conhecida por qualquer trader de Forex. É confiável e não é complicada. Ela é bastante funcional, com cotações em tempo real, robôs de negociação e análise técnica. Tudo isso em um único aplicativo. Para alcançar bons resultados, precisamos aproveitar tudo isso.

Como funciona: Python pega os dados do MetaTrader 5, os processa com suas bibliotecas, e, em seguida, envia os comandos de volta ao MetaTrader 5 para executar as operações. Claro, há desafios. Mas esses aplicativos são muito produtivos quando utilizados em conjunto.

Para trabalhar com o MetaTrader 5 por meio do Python, há uma biblioteca oficial dos desenvolvedores. Para ativá-la, basta instalá-la. Com ela, é possível obter cotações, enviar ordens e gerenciar posições. Tudo funciona como no terminal, só que agora com o poder do Python.

Quais funções e possibilidades temos agora? Muitas. Podemos, por exemplo, automatizar a negociação e fazer uma análise complexa de dados históricos. Podemos até criar nossa própria plataforma de negociação. Embora seja algo destinado a usuários mais avançados, é totalmente possível.


Configuração do ambiente: instalação das bibliotecas necessárias e conexão com o MetaTrader 5

Vamos iniciar nosso processo de trabalho com o Python. Se você ainda não o instalou, acesse python.org. Faça o download e a instalação. Também é necessário marcar a opção "ADD TO PATH".

Nosso próximo passo são as bibliotecas. Precisaremos de algumas. A principal delas é o MetaTrader5. A instalação não exige conhecimentos avançados.

Abrimos o terminal e digitamos:

pip install MetaTrader5 pandas numpy

Pressionamos Enter e vamos tomar um café. Ou um chá. Ou o que você preferir.

Tudo instalado? Agora, é hora de se conectar ao MetaTrader 5.

Primeiro de tudo, é preciso instalar o próprio MetaTrader 5. Você o baixa no site da sua corretora. Só é importante lembrar o caminho do terminal. Geralmente é algo como "C:\ProgramFiles\MetaTrader 5\terminal64.exe". Guarde esse caminho, ele será útil.

Agora abrimos o Python e escrevemos:

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!")

Se tudo rodou, ótimo. Então, podemos seguir para a próxima parte.


Estrutura do nosso código: funções principais e suas finalidades

Começamos com os imports. Aqui temos importações como: MetaTrader5, pandas, datetime, pytz... Em seguida, vêm as funções.

  • A primeira função é remove_duplicate_indices. Ela garante que não haja duplicatas nos nossos dados.
  • Depois vem a get_mt5_data. Ela acessa as funções do MetaTrader 5 e extrai os dados necessários. Especificamente das últimas 24 horas.
  • get_currency_data é uma função muito interessante. Ela chama a get_mt5_data para um monte de pares de moedas. AUDUSD, EURUSD, GBPJPY e muitos outros.
  • A próxima é calculate_synthetic_prices. Essa função é um verdadeiro marco. Trabalhando com pares de moedas, ela gera centenas de preços sintéticos.
  • analyze_arbitrage busca oportunidades de arbitragem, comparando preços reais com os sintéticos. E grava todos os achados em um arquivo CSV.
  • open_test_limit_order envolve mais uma parte eficiente do nosso código. Quando encontra uma oportunidade de arbitragem, essa função abre uma ordem de teste. Mas limita a no máximo 10 operações abertas ao mesmo tempo.

E por fim, a função main. Ela coordena todo esse processo, chamando as funções na ordem correta.

E no final, um ciclo infinito. Ele dispara todo o ciclo a cada 5 minutos, mas apenas durante o horário de negociação. Essa é a nossa estrutura. Simples, e ao mesmo tempo eficaz. 


Coleta de dados no MetaTrader 5: função get_mt5_data

A primeira tarefa é coletar dados do terminal.

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)

Perceba que usamos o horário UTC. Porque no mundo do Forex não há espaço para confusão com fusos horários.

Agora o principal: obtenção de ticks:

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

Pegamos os dados? Ótimo. Agora precisamos processá-los. Para isso, usamos pandas:

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

Voilà! Agora temos nosso próprio DataFrame com os dados. Já prontos para análise.

Mas e se algo deu errado? Sem pânico, nossa função já previu isso também:

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

Ela apenas avisa sobre o problema e retorna None. Essa é a nossa função get_mt5_data. 


Trabalhando com múltiplos pares de moedas: função get_currency_data

Vamos mergulhar mais fundo no sistema, mais especificamente na função get_currency_data. Vamos dar uma olhada no código:

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

Tudo começa com a definição dos pares de moedas. Na lista temos AUDUSD, EURUSD, GBPJPY e outros instrumentos bem conhecidos por nós.

Vamos para o próximo passo. A função cria um dicionário vazio chamado data. Mais adiante, ele será preenchido com os dados necessários.

Agora nossa função começa a operar. Ela percorre a lista de pares de moedas. Para cada par, ela chama a get_mt5_data. Se get_mt5_data retornar dados (e não None), nossa função pega apenas o mais importante: horário, bid e ask.

E então, finalmente, o grand finale. A função retorna o dicionário preenchido com dados. 

Assim temos o get_currency_data. Pequena, poderosa, simples, mas eficaz.


Cálculo de 2000 preços sintéticos: estratégia e implementação

Agora, mergulhamos no núcleo do nosso sistema: a função calculate_synthetic_prices. É por meio dela que geramos nossos dados sintetizados.

Vamos dar uma olhada nesse código:

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)


Análise de oportunidades de arbitragem: função analyze_arbitrage

O que acontece a seguir? Primeiro, criamos um dicionário vazio chamado synthetic_prices. Também vamos preenchê-lo com dados. Depois percorremos todos os dados e removemos índices duplicados para evitar erros futuros.

O próximo passo é a lista pairs. Esses são os pares de moedas que usaremos para a síntese. Em seguida, começa mais um processo. Iniciamos um laço por todos os pares. Para cada par, calculamos o preço sintético de duas maneiras:

  1. Dividimos o bid do primeiro par pelo ask do segundo.
  2. Dividimos o bid do primeiro par pelo bid do segundo.

E a cada vez, incrementamos nosso method_count. No fim, temos não 1000, nem 1500, mas incríveis 2000 preços sintéticos!

É assim que funciona a função calculate_synthetic_prices. Ela não apenas calcula preços, ela basicamente cria novas oportunidades. Essa função entrega ótimos resultados na forma de oportunidades de arbitragem!


Visualização dos resultados: salvando dados em CSV

Vamos analisar a função analyze_arbitrage. Ela não apenas examina os dados, ela encontra o que é necessário no meio do fluxo de números. Vamos dar uma olhada nela:

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

Primeiro, nossa função cria um dicionário vazio chamado spreads. Também vamos preenchê-lo com dados.

Seguimos para o próximo passo. A função percorre todos os pares de moedas e seus equivalentes sintéticos. Para cada par, ela calcula o spread, que é a diferença entre o preço real de bid e o preço sintético.

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

Essa linha tem um papel bem importante. Ela identifica a diferença entre o preço real e o sintético. Se essa diferença for positiva, teremos uma oportunidade de arbitragem.

Para obter resultados mais relevantes, usamos o valor 0.00008:

arbitrage_opportunities = pd.DataFrame(spreads) > 0.00008

Essa linha elimina todas as oportunidades com menos de 8 pontos. Assim, conseguimos focar nas oportunidades com maior chance de lucro.

E agora o próximo passo:

arbitrage_opportunities.to_csv('arbitrage_opportunities.csv')

Agora todos os nossos dados são salvos em um arquivo CSV. Podemos estudá-los, analisá-los, construir gráficos etc., ou seja, realizar um trabalho produtivo. E tudo isso graças à função analyze_arbitrage. Ela não apenas analisa, ela busca, encontra e salva oportunidades de arbitragem.


Abertura de ordens de teste: função open_test_limit_order

Agora vamos olhar a próxima função open_test_limit_order. É ela que vai abrir nossas ordens.

Vamos dar uma olhada:

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

Primeiro, nossa função tenta se conectar ao terminal MetaTrader 5. Depois, ela verifica se o instrumento que queremos negociar realmente existe.

O próximo trecho de código:

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

Essa verificação garante que não abramos posições em excesso.

O próximo passo é criar a solicitação para abrir uma ordem. Há vários parâmetros aqui. Tipo de ordem, volume, preço, desvio, número mágico, comentário... Se tudo der certo, a função nos informa. Caso contrário, uma mensagem de erro aparece.

É assim que funciona a função open_test_limit_order. Essa é nossa ligação com o mercado, ela atua como uma corretora.


Limitações de horário de negociação: operando em horários específicos

Agora vamos falar sobre o horário de negociação. 

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

O que acontece aqui? Nosso sistema verifica o horário. Se for entre 23:30 e 5:00 da manhã, ele entende que não é hora de operar e entra em modo de espera por 5 minutos. Depois ativa-se, checa a hora novamente e, se ainda for cedo, espera mais uma vez.

Por que isso? Há alguns motivos. Primeiro, a liquidez. À noite, ela geralmente é menor. Segundo, os spreads. À noite, eles aumentam. Terceiro, as notícias. As mais importantes costumam sair no horário comercial.


Laço de execução e tratamento de erros

Vamos analisar a função main. Ela é como o capitão do navio, só que, ao invés do leme, usa o teclado. O que ela faz? Ah, simples:

  1. Coleta os dados
  2. Calcula os preços sintéticos 
  3. Busca oportunidades de arbitragem 
  4. Abre ordens

E ainda há um pequeno tratamento de erros. 

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}")


Escalabilidade do sistema: adicionando novos pares de moedas e métodos

Quer adicionar um novo par de moedas? Basta colocá-lo nesta lista:

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

O sistema agora sabe sobre o novo par. E quanto a novos métodos de cálculo?

def calculate_synthetic_prices(data):
    # ... существующий код ...
    
    # Добавляем новый метод
    synthetic_prices[f'{pair1}_{method_count}'] = data[pair1]['ask'] / data[pair2]['bid']
    method_count += 1


Testes e backtesting do sistema de arbitragem

Vamos falar sobre o backtesting. Esse é um ponto realmente importante para qualquer sistema de negociação. E nosso sistema de arbitragem não é exceção.

O que fizemos? Pegamos nossa estratégia e a passamos por dados históricos. Por quê? Para entender o quão eficaz ela é. Nosso código começa com get_historical_data. Essa função extrai dados antigos do MetaTrader 5. Sem esses dados, infelizmente, não conseguimos trabalhar de forma produtiva.

Depois vem a calculate_synthetic_prices. Aqui calculamos as cotações sintéticas das moedas. Essa é a parte-chave da nossa estratégia de arbitragem. Analyze_arbitrage é nosso detector de oportunidades. Ele compara os preços reais com os sintéticos. Encontra a diferença, e aí podemos obter lucro potencial. Já o simulate_trade é praticamente o processo de negociação em si. Mas acontece em modo de teste. Um processo muito importante: é melhor errar na simulação do que perder dinheiro real.

Por fim, backtest_arbitrage_system junta tudo e roda nossa estratégia sobre os dados históricos. Dia após dia, operação após operação.

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

Por que isso é importante? Porque o backtesting mostra o quão eficaz é o nosso sistema. Ele dá lucro, ou consome o capital? Qual o rebaixamento? Qual o percentual de operações vencedoras? Tudo isso só descobrimos com o backtest.

Claro que resultados passados não garantem o futuro. O mercado muda. Mas sem o backtest não temos nenhuma referência. Com ele, já temos uma noção do que esperar. E tem mais: o backtesting ajuda a otimizar o sistema. Mudamos os parâmetros, observamos o resultado. Mudamos de novo, analisamos de novo. Assim, passo a passo, tornamos o sistema melhor.

Aqui está o resultado do funcionamento da nossa estratégia no backtest:

Aqui está o teste da estratégia no MetaTrader 5:

E aqui está o código do EA no MQL5 para essa estratégia:

//+------------------------------------------------------------------+
//|                                                 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;
}


Caminhos de melhoria e legalidade da estratégia para corretoras, ou como não prejudicar o provedor de liquidez usando ordens limitadas

No entanto, nosso sistema tem outros desafios. Corretoras e provedores de liquidez muitas vezes veem essas estratégias com desaprovação. Por quê? Porque estamos, basicamente, retirando liquidez necessária do mercado. Eles até criaram um termo específico para isso: "Toxic Order Flow". 

É um problema real. Com nossas ordens a mercado, estamos literalmente drenando liquidez do sistema. E essa liquidez é essencial: tanto para os grandes players quanto para os pequenos traders. Naturalmente, isso gera consequências.

Como agir nessa situação? Existe um meio-termo: as ordens limitadas. 

Mas nem tudo se resolve com isso: a marcação de Toxic Order Flow nem sempre é por causa da retirada instantânea de liquidez, e sim pela carga operacional imposta por esse fluxo intenso de ordens. Esse problema eu ainda não resolvi. Por exemplo, gastar 100 dólares (vamos assumir esse valor teoricamente) para manter um fluxo imenso de operações de um arbitrador e receber apenas 50 de comissão não é viável. Então talvez a chave esteja em grandes volumes e tamanhos de lote elevados, além de alta rotatividade. Nesse caso, será que as corretoras até ofereceriam rebates?

Agora sobre o código. Como podemos melhorá-lo? Em primeiro lugar, podemos adicionar uma função para trabalhar com ordens limitadas. Aqui há bastante coisa a ser feita, pois é necessário planejar bem a lógica de espera e o cancelamento de ordens não executadas.

Uma ideia interessante para aprimorar o sistema é o aprendizado de máquina. Acredito que seja possível treinar nosso sistema para prever quais oportunidades de arbitragem têm maior probabilidade de sucesso. 


Considerações finais

Vamos recapitular. Criamos um sistema que identifica oportunidades de arbitragem. Lembre-se de que nosso sistema não é uma solução mágica para todos os seus problemas financeiros. 

Exploramos o backtesting. Trata-se de um trabalho com dados históricos que, melhor ainda, permite visualizar como o sistema teria se comportado no passado. No entanto, nunca se esqueça: resultados passados não garantem o futuro. O mercado é um mecanismo complexo e em constante transformação.

Mas sabe o que é mais importante? Não é o código. Nem os algoritmos. É você. Seu desejo de aprender, de experimentar, de errar e tentar novamente. Isso sim é algo realmente valioso.

Então não pare por aqui. Este sistema é apenas o começo da sua jornada no mundo do trading algorítmico. Use-o como ponto de partida para novas ideias e estratégias. E lembre-se: no trading, como na vida, o mais importante é o equilíbrio. É preciso encontrar um equilíbrio entre risco e cautela, ganância e razão, complexidade e simplicidade.

Boa sorte nessa jornada fascinante, e que seus algoritmos estejam sempre um passo à frente do mercado!

Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/15964

Do básico ao intermediário: Indicador (IV) Do básico ao intermediário: Indicador (IV)
Neste artigo, vermos como é fácil de criar e implementar uma metodologia operacional, visando colorir candles. Sendo este um conceito, que diversos operadores apreciam imensamente. Porém, é preciso se tomar cuidado ao implementar tal tipo de coisa. Isto para que as barras, ou candles, mantenham a sua aparência original. Visando assim não prejudicar a leitura que muitos operadores fazem candle a candle.
Busca de padrões arbitrários em pares de moedas no Python com o uso do MetaTrader 5 Busca de padrões arbitrários em pares de moedas no Python com o uso do MetaTrader 5
Existem padrões repetitivos e regularidades no mercado cambial? Decidi criar meu próprio sistema de análise de padrões usando Python e MetaTrader 5. Uma espécie de simbiose entre matemática e programação para conquistar o Forex.
Simulação de mercado (Parte 14): Sockets (VIII) Simulação de mercado (Parte 14): Sockets (VIII)
Muitos poderiam sugerir, que deveríamos abandonar o Excel, e usar o Python pura e simplesmente. Fazendo uso de alguns pacotes que permitiriam ao Python criar um arquivo de Excel, para que pudéssemos analisar os resultados depois. Mas como foi dito no artigo anterior, apesar desta solução ser a mais simples, pelo ponto de vista de muitos programadores. Ela de fato, não será bem vista, pelos olhos de alguns usuários. E nesta história toda, o usuário tem sempre razão. Você como programador deve, encontrar alguma forma ou alguma maneira de fazer as coisas funcionarem.
Construção de previsões econômicas: potencialidades do Python Construção de previsões econômicas: potencialidades do Python
Como utilizar os dados econômicos do Banco Mundial para fazer previsões? O que acontece se combinarmos modelos de IA com economia?