English Русский 中文 Español Deutsch 日本語
preview
Arbitragem estatística com ações cointegradas (Parte 3): Configuração do banco de dados

Arbitragem estatística com ações cointegradas (Parte 3): Configuração do banco de dados

MetaTrader 5 — Sistemas de negociação |
23 0
Jocimar Lopes
Jocimar Lopes

Introdução

No artigo anterior desta série (Parte 2), realizamos o backtest de uma estratégia de arbitragem estatística baseada em uma cesta de ações cointegradas do setor de microprocessadores, negociadas na Nasdaq. Primeiro, selecionamos, entre centenas de tickers, aqueles que apresentavam maior correlação com a Nvidia. Em seguida, aplicamos o teste de Johansen para avaliar a cointegração do grupo selecionado e os testes ADF e KPSS para verificar a estacionariedade do spread. Por fim, obtivemos os pesos relativos da carteira a partir do autovetor de Johansen correspondente ao primeiro posto de cointegração. Os resultados do backtest foram promissores.

Entretanto, dois ou mais ativos podem ter permanecido cointegrados durante os últimos dois anos e começar a perder essa relação já amanhã. Em outras palavras, não há garantia de que um par ou grupo de ativos continue cointegrado. Mudanças na gestão de uma empresa, nas condições macroeconômicas ou no próprio setor podem afetar os fundamentos que originalmente sustentavam a cointegração entre os ativos. O contrário também pode acontecer: ativos que antes não eram cointegrados podem passar a apresentar cointegração a qualquer momento, pelos mesmos motivos. O mercado é um "enigma em constante transformação". Precisamos nos adaptar a essas mudanças. [PALOMAR, 2025]

Os pesos relativos dos ativos em uma cesta de ações cointegradas mudam quase continuamente. Esses pesos determinam não apenas o tamanho das nossas ordens (a quantidade de ações negociadas), mas também sua direção (compra ou venda). Portanto, precisamos acompanhar essas mudanças e ajustar nossa estratégia. Embora a cointegração represente uma relação de longo prazo, os pesos da carteira variam constantemente. Por isso, precisamos verificá-los com maior frequência e atualizar o modelo assim que houver alterações. Quando identificamos que nosso modelo está desatualizado, precisamos agir imediatamente, substituindo-o por um modelo atualizado.

Nosso Expert Advisor (EA) deve ser capaz de identificar, em tempo real, se os pesos da carteira utilizados pelo EA continuam atualizados ou se já sofreram alterações. Caso tenham mudado, o EA precisa receber os novos pesos o mais rápido possível. Além disso, deve verificar se o próprio modelo continua válido. Se não estiver mais válido, o EA precisa receber informações sobre quais ativos devem ser substituídos e realizar imediatamente a rotação dos ativos na carteira atual.

Utilizamos a integração do MetaTrader 5 com Python e as funções estatísticas especializadas da biblioteca statsmodels, mas, até agora, trabalhamos apenas com dados em tempo real, obtendo as cotações (dados de preços) conforme a necessidade. Essa abordagem é conveniente durante a fase de pesquisa devido à sua simplicidade. Entretanto, se pretendemos realizar a rotação dos ativos da carteira, atualizar os modelos ou os pesos da carteira, precisamos considerar o armazenamento dos dados. Em outras palavras, devemos começar a armazená-los em um banco de dados, pois baixar novamente as cotações sempre que precisamos delas se torna pouco prático. Além disso, talvez precisemos investigar relações entre diferentes classes de ativos e entre instrumentos que não participaram dos nossos testes iniciais de cointegração.

Um banco de dados de alta qualidade, escalável e rico em metadados é a base de qualquer projeto sério de arbitragem estatística. Como a modelagem de um banco de dados depende, em grande parte, das necessidades específicas de cada projeto (um bom banco de dados é aquele que atende aos requisitos do negócio), neste artigo vamos explorar uma das possíveis abordagens para criar um banco de dados voltado à arbitragem estatística.


Quais perguntas nosso banco de dados deve responder?

Ao tentar desenvolver um "framework de baixo custo para arbitragem estatística", ou seja, um sistema acessível ao trader de varejo comum, que dispõe de um notebook convencional e uma conexão à internet com velocidade convencional, enfrentamos diversas dificuldades decorrentes da falta de conhecimento especializado em áreas como estatística e desenvolvimento de software. A modelagem de bancos de dados NÃO é exceção. Trata-se de uma área extensa, sobre a qual seria possível escrever livros inteiros sem esgotar o assunto. O ideal, naturalmente, seria contar com um ou mais especialistas para cuidar da modelagem, implementação e manutenção do banco de dados.

Entretanto, como estamos desenvolvendo esse framework para o trader comum, precisamos trabalhar com os recursos disponíveis: estudar livros, consultar fóruns especializados e outros materiais, aprender com profissionais experientes, fazer tentativas, cometer erros, experimentar, assumir riscos e estar preparados para modificar a arquitetura do banco de dados caso ela se mostre inadequada. Precisamos de flexibilidade e de uma abordagem gradual. É melhor começar com uma estrutura simples e expandi-la de baixo para cima (bottom-up), em vez de adotar uma abordagem de cima para baixo (top-down), evitando assim uma complexidade desnecessária.

Em última análise, nosso banco de dados deve ser capaz de responder a uma pergunta muito simples: quais ativos devemos negociar agora para obter o maior lucro possível?

Retomando o artigo anterior, no qual a direção e o tamanho das ordens eram determinados pelos pesos dos ativos na carteira, uma possível resposta do banco de dados seria semelhante a esta:

Símbolos Pesos Timeframe
 

"MU", "NVDA", "MPWR", "MCHP"

2.699439, 1.000000, -1.877447, -2.505294 D1

Tabela 1 — Exemplo de resposta esperada de uma consulta para atualização do modelo em tempo real

Se nosso banco de dados conseguir fornecer essas informações relativamente simples, com uma frequência de atualização adequada, teremos tudo o que precisamos para manter a negociação contínua com a estratégia tão otimizada quanto possível.



Atualização do banco de dados como serviço

Até agora, realizamos a análise dos dados utilizando cotações em tempo real obtidas do terminal MetaTrader 5 por meio de código Python (tecnicamente, na maior parte do tempo, utilizamos cotações que já estavam armazenadas pelo mecanismo interno do terminal). Depois de definir os símbolos e os pesos da carteira, atualizávamos manualmente nosso EA, configurando os novos símbolos e/ou pesos.

A partir de agora, vamos separar a análise de dados do terminal e passar a utilizar os dados armazenados em nosso banco de dados. Isso permitirá atualizar o EA assim que novos pesos da carteira estiverem disponíveis, interromper as negociações caso a relação de cointegração deixe de existir ou migrar para outro grupo de instrumentos com perspectivas mais favoráveis. Em outras palavras, queremos melhorar a exposição ao mercado de cada instrumento, atualizando os pesos da carteira em tempo real e/ou realizando a rotação dos ativos sempre que os resultados da análise indicarem essa necessidade.

Para atualizar o banco de dados, vamos implementar um serviço do MetaTrader 5.

De acordo com a documentação do MetaTrader 5:

  • Os serviços não estão vinculados a um gráfico específico.

  • Os serviços são carregados automaticamente após a inicialização do terminal, desde que estivessem ativos quando ele foi fechado.

  • Os serviços são executados em threads próprias.

Com isso, podemos concluir que um serviço é a solução ideal para manter o banco de dados funcionando adequadamente. Se o serviço estiver ativo no momento do encerramento da sessão de negociação e do fechamento do terminal, sua execução será retomada automaticamente na próxima inicialização, independentemente do gráfico ou símbolo que estiver aberto. Além disso, como o serviço é executado em uma thread independente, não interfere na execução de outros serviços, indicadores, scripts ou EAs, nem depende deles para funcionar.

Assim, nosso fluxo de trabalho será o seguinte:

  1. Toda a análise de dados será realizada em Python, fora do ambiente do MetaTrader 5. Para executar a análise, carregaremos os dados históricos e os armazenaremos no banco de dados.
  2. Sempre que houver uma alteração na composição da carteira ativa, com a inclusão ou remoção de um símbolo, atualizaremos os parâmetros de entrada do serviço, fornecendo um array de símbolos e outro de timeframes.
  3. Sempre que houver uma alteração na composição da carteira ativa, com a inclusão ou remoção de um símbolo, também atualizaremos os parâmetros de entrada do EA.

Por enquanto, as atualizações das etapas 2 e 3 serão realizadas manualmente. Posteriormente, automatizaremos essas etapas.


Configuração do banco de dados

Durante a preparação deste artigo, confirmei mais uma vez que a ferramenta ideal para essa tarefa seria um banco de dados colunar especializado em séries temporais. Existem diversas soluções no mercado que atendem a esses requisitos, tanto pagas quanto gratuitas, proprietárias ou de código aberto. Essas soluções foram desenvolvidas para lidar com cargas de trabalho altamente especializadas e volumes enormes de dados, oferecendo tempos de resposta inferiores a um segundo tanto na ingestão de dados quanto na execução de consultas em tempo real.

No entanto, em nosso caso, a escalabilidade não é a prioridade. Nosso foco está na simplicidade e na possibilidade de utilização por um único usuário, sem depender de uma equipe de administradores de bancos de dados (DBAs) altamente qualificados e especialistas em sistemas de armazenamento de séries temporais. Por isso, começaremos pela solução mais simples, cientes das limitações dessa escolha e de que o sistema evoluirá conforme necessário.

Começaremos utilizando o banco de dados SQLite integrado ao MetaTrader 5. Há diversos materiais disponíveis sobre como criar e utilizar esse banco de dados no ambiente do MetaTrader 5. Podemos encontrá-los:

Se você pretende utilizar esse banco de dados SQLite de forma mais aprofundada, é altamente recomendável estudar todos esses materiais. A seguir, apresentamos um resumo das etapas necessárias para nosso caso de uso específico, acompanhado de algumas explicações sobre as decisões adotadas.


Esquema do banco de dados

O arquivo db-setup.mq5, disponibilizado com este artigo, é um script em MQL5 que recebe dois parâmetros de entrada: o nome do arquivo do banco de dados SQLite e o nome do arquivo que contém o esquema do banco de dados. Os valores padrão são statarb-0.1.db e schema-0.1.sql, respectivamente.

ATENÇÃO: é altamente recomendável manter o arquivo do esquema do banco de dados sob controle de versão e evitar duplicá-lo no sistema de arquivos. O MetaTrader 5 já conta com um sistema robusto de controle de versão integrado.

Ao executar o script com os parâmetros padrão, será criado um banco de dados SQLite na pasta MQL5/Files/StatArb do seu terminal (NÃO na pasta compartilhada), contendo todas as tabelas e campos descritos a seguir. O script também criará um arquivo chamado output.sql na pasta MQL5/Files, destinado exclusivamente à depuração. Caso ocorra algum problema, você poderá examinar esse arquivo para entender como o sistema está lendo o esquema do banco de dados. Se tudo funcionar corretamente, o arquivo poderá ser excluído sem problemas.

Como alternativa, você pode criar o banco de dados por outros meios e carregar o esquema utilizando qualquer cliente SQLite3, a interface do MetaEditor, o Windows PowerShell ou a linha de comando do SQLite3. Ainda assim, recomendamos utilizar o script fornecido para criar o banco de dados pelo menos na primeira vez. Posteriormente, você poderá adaptar o procedimento às suas necessidades.

Esquema do banco de dados

O esquema inicial do banco de dados contém apenas quatro tabelas, duas das quais são estruturas preliminares reservadas para etapas futuras. Portanto, neste momento, utilizaremos somente as tabelas "symbol" e "market_data".

A Figura 1 apresenta o diagrama entidade-relacionamento (ERD) desse esquema inicial.

Figura 1. Diagrama entidade-relacionamento (ERD) do esquema inicial

Figura 1. Diagrama entidade-relacionamento (ERD) do esquema inicial

do banco de dados Como o próprio nome sugere, a tabela "corporate_event" foi criada para armazenar eventos corporativos relacionados às empresas que compõem nossa carteira, como pagamentos de dividendos e seus respectivos valores, desdobramentos de ações, recompras de ações, fusões, entre outros. Por enquanto, não utilizaremos essa tabela.

A tabela "trade" armazenará nossas operações. Ao reunir esses registros, teremos um conjunto de dados exclusivo, que poderá ser utilizado para agregação e análise. Essa tabela só será utilizada quando começarmos a negociar.

A tabela "market_data" armazenará os dados OHLC, incluindo todos os campos da estrutura MqlRates. Ela utiliza uma chave primária composta (symbol_id, timeframe e timestamp) para garantir que cada registro seja único. A tabela "market_data" está vinculada à tabela "symbol" por meio de uma chave estrangeira.

Como podemos observar, essa é a única tabela que utiliza uma chave primária composta. Em todas as demais tabelas, a chave primária é uma marca de tempo armazenada como INTEGER. Há um motivo para essa escolha. Segundo a documentação do SQLite3:

"Os dados das tabelas com rowid são armazenados em uma árvore B que contém um registro para cada linha da tabela, utilizando o valor de rowid como chave. Isso significa que a recuperação ou ordenação dos registros por rowid é rápida. A busca por um registro com um rowid específico ou por todos os registros cujos valores de rowid estejam em determinado intervalo é aproximadamente duas vezes mais rápida do que uma busca equivalente por qualquer outra CHAVE PRIMÁRIA ou valor indexado.

(...) Se uma tabela com rowid tiver uma chave primária composta por uma única coluna, cujo tipo declarado seja "INTEGER", independentemente de maiúsculas ou minúsculas, essa coluna se tornará um alias de rowid. Esse tipo de coluna é geralmente chamado de "chave primária inteira".

(...) É possível obter consultas 'aproximadamente duas vezes mais rápidas' utilizando INTEGER como chave primária. As marcas de tempo Unix epoch podem ser inseridas como valores INTEGER em bancos de dados SQLite3."

(...) "O SQLite armazena números inteiros de 64 bits utilizando a representação em complemento de dois¹. Isso permite armazenar valores no intervalo de -9223372036854775808 a +9223372036854775807, inclusive. Os números inteiros nesse intervalo são representados com exatidão. (Documentação do SQLite3)".

Portanto, podemos substituir nossas strings de data e hora por marcas de tempo Unix epoch e utilizá-las como chaves primárias, obtendo um ganho de velocidade que supera a própria velocidade da luz. 🙂

As tabelas a seguir apresentam a documentação completa do esquema do banco de dados (dicionário de dados), que servirá como referência para nossa equipe e para consultas futuras.

Tabela: symbol

Armazena metadados dos instrumentos financeiros negociados ou monitorados.

Campo Tipo de dado Null Chave Descrição
symbol_id INTEGER NO PK Identificador único de cada instrumento financeiro.
ticker TEXT(≤10) NO
Ticker do ativo (por exemplo, "AAPL", "MSFT").
exchange TEXT(≤50) NO
Bolsa em que o ativo é cotado (por exemplo, "NASDAQ", "NYSE").
asset_type TEXT(≤50) YES
Tipo de ativo (por exemplo, "Equity", "ETF", "FX", "Crypto").
sector TEXT(≤50) YES

Classificação do setor econômico (por exemplo, "Technology", "Healthcare").
industry TEXT(≤50) YES

Classificação do ramo de atividade dentro do setor econômico.
currency TEXT(≤50) YES   Moeda em que o ativo é denominado (por exemplo, "EUR", "USD").

Tabela 2 – Descrição do dicionário de dados da tabela 'symbol' (v0.1)

Tabela: corporate_event

Registra eventos que afetam os ativos, como pagamentos de dividendos, desdobramentos de ações ou divulgações de resultados financeiros.

Campo Tipo de dado Null Chave Descrição Exemplo
tstamp INTEGER NO PK Marca de tempo Unix em que o evento entra em vigor. 1678905600
event_type TEXT ENUM {'dividend', 'split', 'earnings'} NO
Tipo de evento corporativo. "dividend"
event_value REAL YES
Valor numérico associado ao evento: • Valor do dividendo por ação • Fator de desdobramento das ações • Lucro por ação (EPS). 0.85, 2.0, 1.35
details TEXT(≤255) YES
Observações adicionais ou informações contextuais. "Q2 dividend payout"
symbol_id INTEGER NO FK Referência a symbol(symbol_id), que vincula o evento ao ativo. 1

Tabela 3 – Descrição do dicionário de dados da tabela 'corporate_event' (v0.1)

Tabela: market_data

Armazena dados OHLCV (open, high, low, close, volume) e as séries temporais relacionadas aos ativos.

Campo Tipo de dado Null Chave Descrição Exemplo
tstamp INTEGER NO PK*
Marca de tempo Unix da barra ou candle. 1678905600
Timeframe TEXT ENUM {M1,M2,M3,M4,M5,M6, M10,M12,M15,M20,M30,H1,H2,H3, H4,H6,H8,H12,D1,W1,MN1} NO
PK*
Granularidade dos dados da série temporal. "M5", "D1"
price_open REAL NO

Preço de abertura da barra. 145.20
price_high REAL
NO

Preço máximo registrado durante a barra. 146.00
price_low REAL
NO

Preço mínimo registrado durante a barra. 144.80
price_close REAL
NO

Preço de fechamento da barra. 145.75
tick_volume INTEGER
YES
Número de ticks (atualizações de preço) registrados durante o período da barra. 200
real_volume INTEGER
YES

Quantidade de unidades ou contratos negociados, quando disponível. 15000
spread REAL
YES

Spread (bid-ask) médio ou instantâneo durante o período da barra. 0.02
symbol_id INTEGER
NO PK*, FK Referência a symbol(symbol_id). 1

Tabela 4 – Descrição do dicionário de dados da tabela 'market_data' (v0.1)

Tabela: trade

Registra operações reais ou simuladas das estratégias de negociação.

Campo Tipo de dado Null Chave Descrição Exemplo
tstamp INTEGER
NO PK Marca de tempo Unix da execução da operação. 1678905600
ticket INTEGER
NO

Ticket/ID da ordem. 20230001
side TEXT ENUM {'buy', 'sell'} NO

Direção da operação. "buy"
quantity INTEGER (>0)
NO

Quantidade de ações ou contratos negociados na operação. 100
price
NO

Preço de execução. 145.50
strategy
YES
Identificador da estratégia de negociação que gerou a operação. "StatArb_Pairs"
symbol_id INTEGER NO FK Referência a symbol(symbol_id). 1

Tabela 5 – Descrição do dicionário de dados da tabela 'trade' (v0.1)

STRICT

Ao examinar o arquivo do esquema, você verá que todas as tabelas foram definidas como STRICT.

CREATE TABLE symbol(
    symbol_id INTEGER PRIMARY KEY,
    ticker TEXT CHECK(LENGTH(ticker) <= 10) NOT NULL,
    exchange TEXT CHECK(LENGTH(exchange) <= 50) NOT NULL,
    asset_type TEXT CHECK(LENGTH(asset_type) <= 50),
    sector TEXT CHECK(LENGTH(sector) <= 50),
    industry TEXT CHECK(LENGTH(industry) <= 50),
    currency TEXT CHECK(LENGTH(currency) <= 10)
) STRICT;

Isso significa que optamos pela tipagem estrita das tabelas, em vez da flexibilidade oferecida pelo sistema de afinidade de tipos do SQLite. Acreditamos que essa decisão poderá evitar problemas no futuro.

"Na instrução CREATE TABLE, a inclusão da palavra-chave "STRICT" ao final, após o parêntese de fechamento ")", faz com que as regras de tipagem estrita sejam aplicadas à tabela." (Documentação do SQLite)

VERIFICAÇÃO DO COMPRIMENTO

Além disso, implementamos verificações de comprimento em alguns campos de texto. Isso ocorre porque, diferentemente de outros sistemas de gerenciamento de bancos de dados (SGBDs), o SQLite não impõe limites de comprimento nem trunca strings com base no valor (n) especificado em CHAR(n) ou VARCHAR(n).

"Observe que os argumentos numéricos entre parênteses após o nome do tipo (por exemplo, "VARCHAR(255)") são ignorados pelo SQLite. O SQLite não impõe restrições ao tamanho de strings, objetos BLOB ou valores numéricos, além do limite global elevado definido por SQLITE_MAX_LENGTH." (Documentação do SQLite)
Implementamos essas restrições para evitar problemas com strings excessivamente longas provenientes de fontes desconhecidas.

E quanto aos índices?

Você pode estar se perguntando por que ainda não criamos nenhum índice. Vamos criá-los quando começarmos a executar consultas, pois assim saberemos exatamente onde serão necessários.


Carga inicial dos dados

A ideia é que o banco de dados seja alimentado automaticamente, sem exigir nossa intervenção, durante a análise de dados realizada por meio da integração do MQL5 com Python. Entretanto, para facilitar essa etapa, disponibilizamos um script Python (db_store_quotes.ipynb) que permite armazenar as cotações de uma lista de símbolos, em um timeframe específico e durante um intervalo de tempo selecionado. Posteriormente, utilizaremos esses dados armazenados em nossas análises, incluindo os testes de correlação, cointegração e estacionariedade.

Figura 2. Tabela 'Symbol' após a carga inicial de dados realizada pelo script Python

Figura 2. Tabela 'Symbol' após a carga inicial de dados realizada pelo script Python

Como podemos observar, a maioria dos metadados dos símbolos está preenchida com 'UNKNOWN'. Isso ocorre porque as funções SymbolInfo disponíveis em Python não fornecem todos os metadados dos símbolos acessíveis pela API do MQL5. Preencheremos essas informações posteriormente.

   # Insert new symbol
    cursor.execute("""
    INSERT INTO Symbol (ticker, exchange, asset_type, sector, industry, currency)
    VALUES (?, ?, ?, ?, ?, ?)
    """, (
        mt5_symbol,
        # some of this data will be filled by the MQL5 DB Update Service
        # because some of them are not provided by the Python MT5 API 
        symbol_info.exchange or 'UNKNOWN',
        symbol_info.asset_class or 'UNKNOWN',
        symbol_info.sector or 'UNKNOWN',
        symbol_info.industry or 'UNKNOWN',
        symbol_info.currency_profit or 'UNKNOWN'
    ))

O caminho do banco de dados deve ser definido em uma variável de ambiente, e utilizamos o módulo python-dotenv para carregá-la. Isso ajuda a evitar problemas com variáveis de ambiente do terminal e/ou do PowerShell que podem não ser reconhecidas pelo editor utilizado.

Logo no início do script, o notebook Jupyter carrega a extensão python-dotenv e o arquivo *.env correspondente.

%load_ext dotenv
%dotenv .env

Um exemplo de arquivo *.env também acompanha este artigo.

# keep this file at the root of your project 
# or in the same folder of the Python script that uses it
STATARB_DB_PATH="your/db/path/here"

A chamada principal está localizada no final do notebook db_store_quotes.ipynb.

symbols = ['MPWR', 'AMAT', 'MU']  # Symbols from Market Watch
timeframe = mt5.TIMEFRAME_M5    # 5-minute timeframe
start_date = '2024-02-01'
end_date = '2024-03-31'
db_path = os.getenv('STATARB_DB_PATH')  # Path to your SQLite database

if db_path is None:
        print("Error: STATARB_DB_PATH environment variable is not set.")
else:
        print("db_path: " + db_path)
# Download historical quotes and store them in the database
        download_mt5_historical_quotes(symbols, timeframe, start_date, end_date, db_path)


Atualizações

O serviço MQL5 executado em segundo plano é a base da manutenção do nosso banco de dados, necessária para automatizar a atualização do modelo e a rotação da carteira. Conforme nosso banco de dados evoluir, esse serviço também precisará ser atualizado.

O serviço se conecta ao arquivo local do banco de dados SQLite ou o cria, caso ainda não exista.

ATENÇÃO: ao criar um banco de dados completamente novo por meio do serviço de atualização, não se esqueça de inicializá-lo utilizando o script db_setup, conforme explicado anteriormente.

Em seguida, o serviço configura as restrições necessárias para garantir a integridade dos dados e entra em um laço contínuo de verificação de

novos dados de mercado. Para cada símbolo e timeframe, ele:

  1. obtém a última barra de preços concluída (incluindo open, high, low, close, volume e spread);
  2. verifica se essa barra já existe no banco de dados;
  3. insere as novas cotações caso ainda não estejam armazenadas.

O serviço executa a inserção dentro de uma transação do banco de dados para garantir a atomicidade. Se ocorrer algum problema, como um erro no banco de dados, ele tentará novamente até atingir o limite configurado (três tentativas, por padrão), com intervalos de um segundo entre elas. O registro de mensagens no log fica a seu critério. O laço faz uma pausa entre as atualizações e só é interrompido quando o serviço é encerrado manualmente.

Agora, vamos examinar alguns de seus componentes.

Nos parâmetros de entrada, você pode definir o caminho do banco de dados no sistema de arquivos, a frequência de atualização em minutos, o número máximo de novas tentativas em caso de falha na inserção e se deseja registrar mensagens de sucesso ou falha no log do EA. Essa última opção pode ser útil quando o código estiver estabilizado após a conclusão do desenvolvimento.

//+------------------------------------------------------------------+
//|   Inputs                                                         |
//+------------------------------------------------------------------+
input string   InpDbPath      = "StatArb\\statarb-0.1.db";  // Database filename
input int      InpUpdateFreq  = 1;     // Update frequency in minutes
input int      InpMaxRetries  = 3;     // Max retries
input bool     InpShowLogs    = true; // Enable logging?

Figura 3. Caixa de diálogo do MetaTrader 5 com os parâmetros de entrada do serviço de atualização do banco de dados

Figura 3. Caixa de diálogo do MetaTrader 5 com os parâmetros de entrada do serviço de atualização do banco de dados

Você deve selecionar os símbolos e os respectivos timeframes cujos dados serão atualizados. A configuração desses parâmetros será automatizada na próxima etapa.

//+------------------------------------------------------------------+
//|   Global vars                                                    |
//+------------------------------------------------------------------+
string symbols[] = {"EURUSD", "GBPUSD", "USDJPY"};
ENUM_TIMEFRAMES timeframes[] = {PERIOD_M5};

Aqui, inicializamos o handle do banco de dados com INVALID_HANDLE. Sua validade será verificada posteriormente, durante a abertura do banco de dados.

// Database handle
int dbHandle = INVALID_HANDLE;

OnStart()

No único manipulador de eventos do serviço, OnStart, criamos um laço infinito e chamamos a função UpdateMarketData, na qual começa a atualização dos dados de mercado. A função Sleep recebe o tempo de espera, em milissegundos, entre as iterações do laço (ou seja, entre as solicitações de atualização das cotações). Para facilitar a configuração, permitimos que o usuário informe esse intervalo em minutos e fazemos a conversão para milissegundos. Além disso, não esperamos que sejam necessárias atualizações em intervalos inferiores a um minuto.

//+------------------------------------------------------------------+
//| Main Service function                                            |
//| Parameters:                                                      |
//|   symbols    - Array of symbol names to update                   |
//|   timeframes - Array of timeframes to update                     |
//|   InpMaxRetries - Maximum number of retries for failed operations|
//+------------------------------------------------------------------+
void OnStart()
  {
   do
     {
      printf("Updating db: %s", InpDbPath);
      UpdateMarketData(symbols, timeframes, InpMaxRetries);
      Sleep(1000 * 60 * InpUpdateFreq); // 60 secs
     }
   while(!IsStopped());
  }

UpdateMarketData()

Começamos chamando a função responsável pela inicialização do banco de dados. Se ocorrer algum erro, retornamos 'false' e devolvemos o controle ao laço principal. Assim, caso haja algum problema na inicialização do banco de dados, você pode manter o serviço em execução enquanto corrige a falha. Na próxima iteração, ele fará uma nova tentativa.

//+------------------------------------------------------------------+
//| Update market data for multiple symbols and timeframes           |
//+------------------------------------------------------------------+
bool UpdateMarketData(string &symbols_array[], ENUM_TIMEFRAMES &time_frames[], int max_retries = 3)
  {
// Initialize database
   if(!InitializeDatabase())
     {
      LogMessage("Failed to initialize database");
      return false;
     }
   bool allSuccess = true;

If the database is initialized (open), we start processing each symbol and timeframe.

// Process each symbol
   for(int i = 0; i < ArraySize(symbols_array); i++)
     {
      string symbol = symbols_array[i];
      // Process each timeframe
      for(int j = 0; j < ArraySize(time_frames); j++)
        {
         ENUM_TIMEFRAMES timeframe = time_frames[j];
         int retryCount = 0;
         bool success = false;

Nesse laço while, controlamos o número máximo de tentativas e chamamos diretamente a função de atualização do banco de dados.

         // Retry logic
         while(retryCount < max_retries && !success)
           {
            success = UpdateSymbolTimeframeData(symbol, timeframe);
            if(!success)
              {
               retryCount++;
               Sleep(1000); // Wait before retry
              }
           }
         if(!success)
           {
            LogMessage(StringFormat("Failed to update %s %s after %d retries",
                                    symbol, TimeframeToString(timeframe), max_retries));
            allSuccess = false;
           }
        }
     }
   DatabaseClose(dbHandle);
   return allSuccess;
  }

UpdateSymbolTimeframeData()

Como a tabela market_data exige symbol_id como chave estrangeira, primeiro precisamos obter esse identificador. Para isso, verificamos se já existe um symbol_id associado ao símbolo na tabela "symbol". Caso seja um símbolo novo, criamos o registro correspondente e obtemos seu symbol_id.

//+------------------------------------------------------------------+
//| Update market data for a single symbol and timeframe             |
//+------------------------------------------------------------------+
bool UpdateSymbolTimeframeData(string symbol, ENUM_TIMEFRAMES timeframe)
  {
   ResetLastError();
// Get symbol ID (insert if it doesn't exist)
   long symbol_id = GetOrInsertSymbol(symbol);
   if(symbol_id == -1)
     {
      LogMessage(StringFormat("Failed to get symbol ID for %s", symbol));
      return false;
     }

Convertemos o timeframe do tipo MQL5 ENUM_TIMEFRAMES para o formato de texto (TEXT) exigido pela tabela.

   string tfString = TimeframeToString(timeframe);
   if(tfString == "")
     {
      LogMessage(StringFormat("Unsupported timeframe for symbol %s", symbol));
      return false;
     }

Copiamos os dados da última barra fechada.

// Get the latest closed bar
   MqlRates rates[];
   if(CopyRates(symbol, timeframe, 1, 1, rates) != 1)
     {
      LogMessage(StringFormat("Failed to get rates for %s %s: %d", symbol, tfString, GetLastError()));
      return false;
     }

Verificamos se esse registro já existe no banco de dados. Em caso afirmativo, registramos a informação no log e retornamos 'true'.

   if(MarketDataExists(symbol_id, rates[0].time, tfString))
     {
      LogMessage(StringFormat("Data already exists for %s %s at %s",
                              symbol, tfString, TimeToString(rates[0].time)));
      return true;
     }

Se for um novo registro de cotação, iniciamos uma transação no banco de dados para garantir a atomicidade da operação. Se ocorrer alguma falha, fazemos o rollback da transação. Se tudo for executado corretamente, confirmamos a transação com um commit e retornamos 'true'.

// Start transaction
   if(!DatabaseTransactionBegin(dbHandle))
     {
      LogMessage(StringFormat("Failed to start transaction: %d", GetLastError()));
      return false;
     }
// Insert the new data
   if(!InsertMarketData(symbol_id, tfString, rates[0]))
     {
      DatabaseTransactionRollback(dbHandle);
      return false;
     }
// Commit transaction
   if(!DatabaseTransactionCommit(dbHandle))
     {
      LogMessage(StringFormat("Failed to commit transaction: %d", GetLastError()));
      return false;
     }
   LogMessage(StringFormat("Successfully updated %s %s data for %s",
                           symbol, tfString, TimeToString(rates[0].time)));
   return true;
  }

InitializeDatabase()

Aqui, abrimos o banco de dados e verificamos se o identificador é válido.

//+------------------------------------------------------------------+
//| Initialize database connection                                   |
//+------------------------------------------------------------------+
bool InitializeDatabase()
  {
   ResetLastError();
// Open database (creates if it doesn't exist)
   dbHandle = DatabaseOpen(InpDbPath, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE);
   if(dbHandle == INVALID_HANDLE)
     {
      LogMessage(StringFormat("Failed to open database: %d", GetLastError()));
      return false;
     }

A diretiva PRAGMA que habilita foreign_keys no SQLite não é estritamente necessária quando utilizamos o banco de dados SQLite integrado, pois sabemos que ele foi compilado com suporte a esse recurso. Trata-se apenas de uma medida de precaução caso você utilize um banco de dados externo.

// Enable foreign key constraints
   if(!DatabaseExecute(dbHandle, "PRAGMA foreign_keys = ON"))
     {
      LogMessage(StringFormat("Failed to enable foreign keys: %d", GetLastError()));
      return false;
     }
   LogMessage("Database initialized successfully");
   return true;
  }

TimeframeToString()

A conversão de MQL5 ENUM_TIMEFRAMES para texto é realizada por meio de uma estrutura switch simples.

//+------------------------------------------------------------------+
//| Convert MQL5 timeframe to SQLite format                          |
//+------------------------------------------------------------------+
string TimeframeToString(ENUM_TIMEFRAMES tf)
  {
   switch(tf)
     {
      case PERIOD_M1:
         return "M1";
      case PERIOD_M2:
         return "M2";
      case PERIOD_M3:
         return "M3";
(...)
      case PERIOD_MN1:
         return "MN1";
      default:
         return "";
     }
  }

MarketDataExists()

Para verificar se os dados de mercado já estão armazenados, executamos uma consulta utilizando a chave primária composta da tabela market_data.

//+------------------------------------------------------------------+
//| Check if market data exists for given timestamp and timeframe    |
//+------------------------------------------------------------------+
bool MarketDataExists(long symbol_id, datetime tstamp, string timeframe)
  {
   ResetLastError();
   int stmt = DatabasePrepare(dbHandle, "SELECT 1 FROM market_data WHERE symbol_id = ? AND tstamp = ? AND timeframe = ? LIMIT 1");
   if(stmt == INVALID_HANDLE)
     {
      LogMessage(StringFormat("Failed to prepare market data existence check: %d", GetLastError()));
      return false;
     }
   if(!DatabaseBind(stmt, 0, symbol_id) ||
      !DatabaseBind(stmt, 1, (long)tstamp) ||
      !DatabaseBind(stmt, 2, timeframe))
     {
      LogMessage(StringFormat("Failed to bind parameters for existence check: %d", GetLastError()));
      DatabaseFinalize(stmt);
      return false;
     }
   bool exists = DatabaseRead(stmt);
   DatabaseFinalize(stmt);
   return exists;
  }

InsertMarketData()

Por fim, para inserir novos dados de mercado (atualização), executamos uma instrução de inserção utilizando a função StringFormat do MQL5, conforme recomendado na documentação.

Tenha cuidado com as valores de texto interpolados na cláusula VALUES. Elas precisam estar entre aspas. Depois você me agradece. 🙂

//+------------------------------------------------------------------+
//| Insert market data into database                                 |
//+------------------------------------------------------------------+
bool InsertMarketData(long symbol_id, string timeframe, MqlRates &rates)
  {
   ResetLastError();
   string req = StringFormat(
                   "INSERT INTO market_data ("
                   "tstamp, timeframe, price_open, price_high, price_low, price_close, "
                   "tick_volume, real_volume, spread, symbol_id) "
                   "VALUES(%d, '%s', %G, %G, %G, %G, %d, %d, %d, %d)",
                   rates.time, timeframe, rates.open, rates.high, rates.low, rates.close,
                   rates.tick_volume, rates.real_volume, rates.spread, symbol_id);
   if(!DatabaseExecute(dbHandle, req))
     {
      LogMessage(StringFormat("Failed to insert market data: %d", GetLastError()));
      return false;
     }
   return true;
  }

Enquanto o serviço estiver em execução...

Figura 4. Navegador do MetaEditor com o serviço de atualização do banco de dados em execução

Figura 4. Navegador do MetaEditor com o serviço de atualização do banco de dados em execução

... você deverá ver algo semelhante na aba Experts do diário do MetaTrader 5.

Figura 5. Aba Experts do MetaTrader 5 mostrando as mensagens do serviço de atualização do banco de dados

Figura 5. Aba Experts do MetaTrader 5 mostrando as mensagens do serviço de atualização do banco de dados

Sua tabela "market_data" deverá ter a aparência mostrada a seguir. Observe que, por enquanto, armazenamos todos os dados de mercado de todos os símbolos e timeframes em uma única tabela. Posteriormente, aprimoraremos essa estrutura conforme a necessidade. Neste momento, ela é mais do que suficiente para começarmos a realizar nossas análises de dados de maneira mais robusta.

Figura 6. Aba SQLite integrada ao MetaEditor mostrando as atualizações do banco de dados

Figura 6. Aba SQLite integrada ao MetaEditor mostrando as atualizações do banco de dados


Conclusão

Neste artigo, vimos como deixar de trabalhar com dados de preços obtidos sob demanda, sem armazenamento persistente, e passar a utilizar a primeira versão do banco de dados do nosso framework de arbitragem estatística. Examinamos os critérios adotados na definição do esquema inicial, os métodos de inicialização e a carga dos primeiros dados.

Agora, todas as tabelas, os campos e os relacionamentos do esquema estão documentados, com descrições, restrições e exemplos.

Também detalhamos as etapas necessárias para manter o banco de dados atualizado por meio de um serviço do MetaTrader 5. Apresentamos a implementação desse serviço e os scripts Python para realizar a carga inicial dos dados de qualquer símbolo (ticker) e timeframe disponível.

Com essas ferramentas, sem precisar escrever uma única linha de código, o trader de varejo comum, público-alvo do nosso framework de arbitragem estatística, pode começar a armazenar não apenas os dados de mercado atuais (cotações de preços), mas também metadados sobre as ações utilizadas em nossa estratégia de cointegração e o histórico das operações.

Esse esquema inicial começará a evoluir já na próxima etapa, quando passaremos a atualizar os pesos da carteira em tempo real e a realizar a rotação dos ativos caso a relação de cointegração da cesta enfraqueça, substituindo e/ou adicionando símbolos sem intervenção manual.


Referências

Daniel P. Palomar (2025). Portfolio Optimization: Theory and Application. Cambridge University Press.

* Uma limitação importante do SQLite em comparação com alguns bancos de dados orientados a séries temporais é a ausência de "as-of joins" (junções temporais por proximidade). Como indexaremos nossos dados de mercado por marcas de tempo, é quase certo que não poderemos utilizar junções entre tabelas no futuro, pois utilizamos marcas de tempo como chaves primárias, e elas raramente, ou talvez nunca, coincidirão em duas ou mais tabelas, enquanto as junções (inner, left e outer joins) dependem dessa correspondência entre os índices. Portanto, ao utilizar essas junções, obteremos conjuntos de resultados vazios (null). Este artigo de blog explica detalhadamente esse problema.

Nome do arquivo Descrição
StatArb/db-setup.mq5

Script MQL5 para criar e inicializar o banco de dados SQLite a partir da leitura do arquivo de esquema schema-0.1.sql.

StatArb/db-update-statarb-0.1.mq5 Serviço MQL5 para atualizar o banco de dados SQLite com os dados das últimas barras de preços fechadas.
StatArb/schema-0.1.sql Arquivo SQL com o esquema do banco de dados (DDL), utilizado na inicialização para criar as tabelas, os campos e as restrições.
db_store_quotes.ipynb Notebook Jupyter com código Python. Arquivo auxiliar para carregar o banco de dados SQLite com as cotações dos símbolos referentes a um intervalo de tempo e timeframe específicos.
.env Exemplo de arquivo com variáveis de ambiente a serem carregadas pelo arquivo auxiliar Python mencionado acima. (Opcional)

Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/19242

Arquivos anexados |
Caminhe em novos trilhos: Personalize indicadores no MQL5 Caminhe em novos trilhos: Personalize indicadores no MQL5
Vou agora listar todas as possibilidades novas e recursos do novo terminal e linguagem. Elas são várias, e algumas novidades valem a discussão em um artigo separado. Além disso, não há códigos aqui escritos com programação orientada ao objeto, é um tópico muito importante para ser simplesmente mencionado em um contexto como vantagens adicionais para os desenvolvedores. Neste artigo vamos considerar os indicadores, sua estrutura, desenho, tipos e seus detalhes de programação em comparação com o MQL4. Espero que este artigo seja útil tanto para desenvolvedores iniciantes quanto para experientes, talvez alguns deles encontrem algo novo.
Arquitetura do sistema de aprendizado de máquina no MetaTrader 5 (Parte 2): Rotulagem de dados financeiros para aprendizado de máquina Arquitetura do sistema de aprendizado de máquina no MetaTrader 5 (Parte 2): Rotulagem de dados financeiros para aprendizado de máquina
Na segunda parte da série "MetaTrader 5 e aprendizado de máquina: guia prático", você entenderá por que rótulos simples podem confundir seus modelos e como aplicar técnicas avançadas, como o método da barreira tripla e a varredura de tendências, para definir variáveis-alvo confiáveis que levem o risco em consideração. Com exemplos práticos de implementações otimizadas em Python desses métodos de alto custo computacional, este guia mostra como transformar dados de mercado ruidosos em rótulos confiáveis, capazes de refletir as condições reais de negociação.
Está chegando o novo MetaTrader 5 e MQL5 Está chegando o novo MetaTrader 5 e MQL5
Esta é apenas uma breve resenha do MetaTrader 5. Eu não posso descrever todos os novos recursos do sistema por um período tão curto de tempo - os testes começaram em 09.09.2009. Esta é uma data simbólica, e tenho certeza que será um número de sorte. Alguns dias passaram-se desde que eu obtive a versão beta do terminal MetaTrader 5 e MQL5. Eu ainda não consegui testar todos os seus recursos, mas já estou impressionado.
Do iniciante ao especialista: Criando um EA animado de notícias em MQL5 (IX) - Gerenciamento de múltiplos símbolos em um único gráfico para negociação baseada em notícias Do iniciante ao especialista: Criando um EA animado de notícias em MQL5 (IX) - Gerenciamento de múltiplos símbolos em um único gráfico para negociação baseada em notícias
A negociação baseada em notícias muitas vezes exige o gerenciamento de várias posições e símbolos em um intervalo muito curto, devido à alta volatilidade. Neste artigo, vamos abordar os desafios de operar com múltiplos símbolos e incorporar essa funcionalidade ao nosso EA "Manchetes de notícias". Acompanhe como a negociação algorítmica com MQL5 pode tornar essas operações mais eficientes e eficazes.