Dominando Operações de Arquivos em MQL5: Do I/O Básico à Construção de um Leitor CSV Personalizado
Introdução
No mundo atual do trading automatizado, dados são tudo. Talvez você precise carregar parâmetros personalizados para sua estratégia, ler uma watchlist de símbolos ou integrar dados históricos de fontes externas. Se você trabalha no MetaTrader 5, ficará satisfeito em saber que o MQL5 torna relativamente simples lidar com arquivos diretamente no seu código.
Mas sejamos honestos: vasculhar a documentação para entender operações com arquivos pode parecer um pouco assustador no começo. É por isso que, neste artigo, vamos detalhar os fundamentos de forma amigável e passo a passo. Depois que cobrirmos o básico – como o “sandbox” do MQL5 protege seus arquivos, como abrir arquivos em modo texto ou binário, e como ler e dividir linhas com segurança – colocaremos tudo em prática construindo uma classe simples de leitor CSV.
Por que arquivos CSV? Porque eles estão em todos os lugares – simples, legíveis por humanos e compatíveis com inúmeras ferramentas. Com um leitor CSV, você pode importar parâmetros externos, listas de símbolos ou outros dados personalizados diretamente para seu Expert Advisor ou script, ajustando o comportamento da sua estratégia sem alterar o código toda vez.
Não vamos sobrecarregar você com cada detalhe minúsculo das funções de arquivo do MQL5, mas cobriremos o que você realmente precisa saber. Ao final, você terá um exemplo claro de como abrir um arquivo CSV em modo texto, como ler suas linhas até o final do arquivo, como dividir cada linha em campos por um delimitador escolhido, como armazenar e recuperar esses campos por nome ou índice de coluna e entender claramente cada etapa.
Aqui está o plano para este artigo:
- Fundamentos das Operações de Arquivos em MQL5
- Projetando a classe leitora de CSV
- Finalizando a implementação da classe leitora de CSV
- Cenários de Testes e Uso
- Conclusão
Fundamentos das Operações de Arquivos em MQL5
Antes de implementarmos nosso leitor CSV, vamos analisar com mais atenção alguns conceitos fundamentais de manipulação de arquivos em MQL5 e ilustrá-los com código. Vamos focar no entendimento das restrições do sandbox, modos de abertura de arquivos, leitura linha a linha e tratamento básico de erros. Ver esses fundamentos na prática tornará mais fácil construir e depurar nosso leitor CSV posteriormente.
Primeiro, vamos entender O Sandbox, Acesso Restrito a Arquivos. O MQL5 impõe um modelo de segurança que restringe as operações com arquivos a determinados diretórios conhecidos como “sandbox”. Normalmente, você só pode ler e escrever arquivos localizados em <TerminalDataFolder>/MQL5/Files. Se você tentar acessar arquivos fora desse diretório, a função FileOpen() falhará.
Por exemplo, se você colocar um arquivo chamado data.csv na pasta MQL5/Files do seu terminal MT5, poderá abri-lo assim:
int fileHandle = FileOpen("data.csv", FILE_READ|FILE_TXT); if(fileHandle == INVALID_HANDLE) { Print("Error: Could not open data.csv. LastError=", _LastError); // _LastError can help diagnose if it's a path or permission issue return; } // Successfully opened the file, now we can read from it.
Você pode se perguntar o que esses códigos de erro significam. Por exemplo, _LastError = 5004 normalmente significa algo como “Arquivo não encontrado” ou “Não é possível abrir o arquivo”, o que geralmente se resume a um erro de digitação no nome do arquivo ou ao arquivo não estar dentro de MQL5/Files. Se você vir outro código, uma verificação rápida na documentação do MQL5 ou nos fóruns da comunidade pode ajudar a decodificar a mensagem. Às vezes é apenas um problema de caminho, às vezes o arquivo está bloqueado por outro programa. Se dados externos forem cruciais para seu EA, considere adicionar uma rápida tentativa adicional ou uma impressão de erro detalhada para que você possa corrigir problemas rapidamente.
Temos muitas opções ao abrir um arquivo. Quando você chama FileOpen(), especifica flags para controlar como o arquivo será acessado. Flags comuns incluem:
- FILE_READ : Abre o arquivo para leitura.
- FILE_WRITE : Abre para escrita.
- FILE_BIN : Modo binário (sem processamento de texto).
- FILE_TXT : Modo texto (lida com quebras de linha e conversões de texto).
- FILE_CSV : Modo texto especial que trata o arquivo como um CSV.
Para leitura de um CSV padrão, FILE_READ|FILE_TXT é um ótimo ponto de partida. O modo texto garante que FileReadString() pare nas quebras de linha, tornando mais simples processar arquivos linha a linha:
int handle = FileOpen("params.txt", FILE_READ|FILE_TXT); if(handle != INVALID_HANDLE) { Print("File opened in text mode."); // ... read lines here ... FileClose(handle); } else { Print("Failed to open params.txt"); }
Uma vez que o arquivo está aberto em modo texto, ler linhas é simples. Use FileReadString() para ler até a próxima quebra de linha. Quando o arquivo termina, FileIsEnding() retorna true. Considere este loop:
int handle = FileOpen("list.txt", FILE_READ|FILE_TXT); if(handle == INVALID_HANDLE) { Print("Error opening list.txt"); return; } while(!FileIsEnding(handle)) { string line = FileReadString(handle); if(line == "" && _LastError != 0) { // If empty line and there's an error, break Print("Read error or unexpected end of file. _LastError=", _LastError); break; } // Process the line Print("Line read: ", line); } FileClose(handle);
Neste trecho, lemos continuamente as linhas até alcançarmos o fim do arquivo. Se ocorrer um erro, interrompemos. Linhas vazias são permitidas, portanto, se quiser ignorá-las, basta verificar if(line=="") continue; . Essa abordagem será útil ao lidar com linhas CSV.
Tenha em mente que arquivos de texto nem sempre são uniformes. A maioria usa \n ou \r\n como quebras de linha, e o MQL5 normalmente lida com isso sem problemas. Ainda assim, se você receber um arquivo de uma fonte incomum, vale a pena verificar se as linhas estão sendo lidas corretamente. Se FileReadString() retornar resultados estranhos (como linhas mescladas), abra o arquivo em um editor de texto e confirme sua codificação e estilo de quebra de linha. Considere também linhas extremamente longas – raro em CSVs pequenos, mas possível. Uma checagem de tamanho ou um trimming pode ajudar a garantir que seu EA não tropece em formatos inesperados.
Para processar dados CSV, você dividirá cada linha em campos com base em um delimitador, geralmente vírgula ou ponto e vírgula. A função StringSplit() do MQL5 ajuda nisso:
string line = "EURUSD;1.2345;Some Comment"; string fields[]; int count = StringSplit(line, ';', fields); if(count > 0) { Print("Found ", count, " fields"); for(int i=0; i<count; i++) Print("Field[", i, "] = ", fields[i]); } else { Print("No fields found in line: ", line); }
Esse código imprime cada campo analisado. Para leitura de CSV, após dividir, você armazenará esses campos na memória para acessá-los depois por índice ou nome da coluna.
Embora StringSplit() funcione muito bem para delimitadores simples, lembre-se de que alguns formatos CSV podem ser mais complexos. Alguns têm campos entre aspas ou delimitadores escapados, o que não estamos tratando aqui. Se seu arquivo for simples – sem aspas ou formatos especiais – StringSplit() é suficiente. Se os campos tiverem espaços sobrando ou pontuação estranha, considere usar StringTrim() após a divisão. Pequenas verificações assim tornam seu EA mais robusto, mesmo que sua fonte de dados adicione pequenos detalhes de formatação.
Muitos arquivos CSV possuem uma linha de cabeçalho que define os nomes das colunas. Se _hasHeader for true em nosso futuro leitor CSV, a primeira linha lida será dividida e armazenada em um hash map que associa nomes de colunas aos seus respectivos índices.
Por exemplo:
// Assume header line: "Symbol;MaxLot;MinSpread" string header = "Symbol;MaxLot;MinSpread"; string cols[]; int colCount = StringSplit(header, ';', cols); // Suppose we have a CHashMap<string,uint> Columns; for(int i=0; i<colCount; i++) Columns.Add(cols[i], i); // Now we can quickly find the index for "MinSpread" or any other column name. uint idx; bool found = Columns.TryGetValue("MinSpread", idx); if(found) Print("MinSpread column index: ", idx); else Print("Column 'MinSpread' not found");Se não houver cabeçalho, dependeremos apenas dos índices numéricos. A primeira linha lida será uma linha de dados, e as colunas serão referenciadas por suas posições.
O hash map ( CHashMap ) para nomes de colunas é um pequeno detalhe que faz grande diferença. Sem ele, sempre que você precisasse de um índice de coluna, teria que percorrer toda a linha de cabeçalho. Com um hash map, TryGetValue() fornece o índice imediatamente. Se uma coluna não for encontrada, você pode retornar um valor de erro — simples e eficiente. Se você se preocupar com colunas repetidas, pode adicionar uma verificação rápida ao ler o cabeçalho e imprimir um aviso caso haja duplicatas. Pequenos aprimoramentos como esse mantêm seu código robusto conforme seus CSVs ficam mais complexos.
Para armazenamento dos dados, manteremos tudo simples: cada linha analisada (após a divisão) se torna uma linha de dados. Usaremos CArrayString para armazenar os campos de uma linha e CArrayObj para armazenar múltiplas linhas:
#include <Arrays\ArrayObj.mqh> #include <Arrays\ArrayString.mqh> CArrayObj Rows; // after splitting line into fields: CArrayString *row = new CArrayString; for(int i=0; i<count; i++) row.Add(fields[i]); Rows.Add(row);
Depois, para recuperar um valor:
// Access row 0, column 1 CArrayString *aRow = Rows.At(0); string val = aRow.At(1); Print("Row0 Col1: ", val);
// Access row 0, column 1 CArrayString *aRow = Rows.At(0); string val = aRow.At(1); Print("Row0 Col1: ", val);
Devemos garantir que os índices sejam válidos antes de acessá-los.
Sempre lide com a possibilidade de arquivos ou colunas ausentes. Por exemplo, se FileOpen() retornar INVALID_HANDLE, registre um erro e encerre. Se um nome de coluna solicitado não existir, retorne um valor padrão. Nossa classe final de leitura CSV encapsulará essas verificações para manter seu código principal do EA organizado.
Juntando tudo — regras do sandbox, abertura de arquivos, leitura de linhas, divisão em campos e armazenamento dos resultados — temos todos os blocos de construção necessários. Nas próximas seções, projetaremos e implementaremos nossa classe leitora de CSV passo a passo, usando esses conceitos. Ao focar em clareza e tratamento de erros agora, tornaremos a implementação subsequente mais suave e confiável.
Projetando a classe leitora de CSV
Agora que revisamos os fundamentos, vamos estruturar nossa classe de leitura CSV e começar a implementar suas partes principais. Criaremos uma classe chamada algo como CSimpleCSVReader que permitirá:
- Abrir um arquivo CSV especificado em modo leitura-texto.
- Se solicitado, tratar a primeira linha como cabeçalho, armazenar os nomes das colunas e criar um mapa de nomes para índices.
- Ler todas as linhas subsequentes para a memória, cada uma dividida em um array de strings (uma por coluna).
- Fornecer métodos para consultar dados por índice ou nome da coluna.
- Retornar valores padrão ou de erro se algo estiver faltando.
Faremos isso passo a passo. Primeiro, vamos considerar as estruturas internas que usaremos:
- Um CHashMap<string,uint> para armazenar a associação nome da coluna -> índice quando houver cabeçalho.
- Um array dinâmico de CArrayString* para as linhas, onde cada CArrayString representa uma linha de campos.
- Algumas propriedades armazenadas como _hasHeader , _filename , _separator , e talvez _rowCount e _colCount.
Usar CArrayObj e CArrayString não é apenas conveniente — evita dores de cabeça com redimensionamento manual de arrays. Arrays nativos são poderosos, mas podem complicar quando os dados crescem. Com CArrayString, adicionar campos é fácil, e CArrayObj permite armazenar uma lista crescente de linhas sem esforço. Enquanto isso, um hash map para nomes de colunas evita procurar repetidamente no cabeçalho. É um design simples e escalável, facilitando sua vida conforme seus CSVs crescem ou suas necessidades de dados evoluem.
Antes de codificar a classe inteira, vamos escrever alguns trechos básicos para ilustrar como abrir um arquivo e ler linhas. Depois, integraremos essas partes no código final da classe. Vamos abrir um arquivo:
int fileHandle = FileOpen("data.csv", FILE_READ|FILE_TXT);
if(fileHandle == INVALID_HANDLE)
{
Print("Error: Could not open file data.csv. Error code=", _LastError);
return;
}
// If we reach here, the file is open successfully.
Este trecho tenta abrir data.csv do diretório MQL5/Files. Se falhar, imprime um erro e retorna. A variável _LastError pode fornecer informações sobre por que o arquivo não pôde ser aberto. Por exemplo, 5004 significa CANNOT_OPEN_FILE. Agora vamos ler o arquivo até o fim:
string line; while(!FileIsEnding(fileHandle)) { line = FileReadString(fileHandle); if(line == "" && _LastError != 0) // If empty line and error occurred { Print("Error reading line. Possibly end of file or another issue. Error=", _LastError); break; } // Process the line here, e.g., split it into fields }
Aqui, repetimos o loop até que FileIsEnding() retorne verdadeiro. Cada iteração lê uma linha e a armazena em outra linha. Se obtivermos uma linha vazia e houver um erro, paramos. Se for genuinamente o fim do arquivo, sairemos do loop naturalmente. Tenha em mente que uma linha completamente vazia no arquivo ainda resultará em uma string vazia, então você pode querer tratar esse cenário dependendo do formato CSV.
Agora, suponha que nosso CSV use ponto e vírgula ( ; ) como delimitador. Podemos fazer:
string line = "Symbol;Price;Volume"; string fields[]; int fieldCount = StringSplit(line, ';', fields); if(fieldCount < 1) { Print("No fields found in line: ", line); } else { // fields now contains each piece of data for(int i=0; i<fieldCount; i++) Print("Field[", i, "] = ", fields[i]); }
StringSplit() retorna o número de partes encontradas. Após essa chamada, fields contém cada token separado por ; . Se a linha fosse EURUSD;1.2345;10000, fields[0] seria EURUSD, fields[1] seria 1.2345, e fields[2] seria 10000.
Se _hasHeader for verdadeiro, a primeira linha que lemos é especial. Vamos dividi-la e armazenar os nomes das colunas em um CHashMap. Por exemplo:
#include <Generic\HashMap.mqh> CHashMap<string,uint> Columns; // columnName -> columnIndex // Assume line is the header line string columns[]; int columnCount = StringSplit(line, ';', columns); for(int i=0; i<columnCount; i++) Columns.Add(columns[i], i);
O hash map para nomes de colunas é um pequeno detalhe que rende grandes benefícios. Sem ele, você percorreria os cabeçalhos de coluna cada vez que quisesse um índice. Com um hash map, uma chamada rápida a TryGetValue() fornece o índice e, se uma coluna não for encontrada, você pode simplesmente retornar um valor padrão. Se aparecerem duplicatas ou nomes estranhos de colunas, você pode detectá-los desde o início. Essa configuração mantém as buscas rápidas e o código limpo, então mesmo que seu CSV dobre de tamanho, recuperar índices de coluna permanece simples.
Agora Columns mapeia cada nome de coluna para seu índice. Se precisarmos do índice de um determinado nome de coluna mais tarde, podemos fazer:
uint idx; bool found = Columns.TryGetValue("Volume", idx); if(found) Print("Volume column index = ", idx); else Print("Column 'Volume' not found");
Cada linha de dados deve ser armazenada em um objeto CArrayString, e manteremos um array dinâmico de ponteiros para essas linhas. Algo como:
#include <Arrays\ArrayString.mqh> #include <Arrays\ArrayObj.mqh> CArrayObj Rows; // holds pointers to CArrayString objects // After reading and splitting a line into fields: // (Assume fields[] array is populated) CArrayString *row = new CArrayString; for(int i=0; i<ArraySize(fields); i++) row.Add(fields[i]); Rows.Add(row);
Depois, para recuperar um valor, faríamos algo como:
CArrayString *aRow = Rows.At(0); // get the first row string value = aRow.At(1); // get second column Print("Value at row=0, col=1: ", value);
Claro, devemos sempre checar os limites para evitar erros fora de faixa.
Vamos acessar colunas por nome ou índice. Se nosso CSV tiver um cabeçalho, podemos usar o mapa Columns para encontrar índices de coluna pelo nome:
string GetValueByName(uint rowNumber, string colName, string errorValue="") { uint idx; if(!Columns.TryGetValue(colName, idx)) return errorValue; // column not found return GetValueByIndex(rowNumber, idx, errorValue); } string GetValueByIndex(uint rowNumber, uint colIndex, string errorValue="") { if(rowNumber >= Rows.Total()) return errorValue; // invalid row CArrayString *aRow = Rows.At(rowNumber); if(colIndex >= (uint)aRow.Total()) return errorValue; // invalid column index return aRow.At(colIndex); }
Este pseudo-código mostra como poderíamos implementar duas funções de acesso. GetValueByName usa o hash map para converter o nome da coluna em um índice, então chama GetValueByIndex. GetValueByIndex verifica limites e retorna valores ou padrões de erro conforme necessário.
Construtor e Destrutor: Podemos encapsular tudo em uma classe. O construtor pode apenas inicializar variáveis internas, e o destrutor deve liberar memória. Por exemplo:
class CSimpleCSVReader { private: bool _hasHeader; string _separator; CHashMap<string,uint> Columns; CArrayObj Rows; public: CSimpleCSVReader() { _hasHeader = true; _separator=";"; } ~CSimpleCSVReader() { Clear(); } void SetHasHeader(bool hasHeader) { _hasHeader = hasHeader; } void SetSeparator(string sep) { _separator = sep; } uint Load(string filename); string GetValueByName(uint rowNum, string colName, string errorVal=""); string GetValueByIndex(uint rowNum, uint colIndex, string errorVal=""); private: void Clear() { for(int i=0; i<Rows.Total(); i++) { CArrayString *row = Rows.At(i); if(row != NULL) delete row; } Rows.Clear(); Columns.Clear(); } };
Esse esboço de classe mostra uma estrutura possível. Ainda não implementamos Load(), mas faremos em breve. Note como mantemos um método Clear() para liberar memória. Após chamar delete row; também devemos Rows.Clear() para resetar o array de ponteiros.
Vamos implementar o método load agora. Load() abrirá o arquivo, lerá a primeira linha (possivelmente cabeçalho), lerá todas as linhas restantes e as analisará:
uint CSimpleCSVReader::Load(string filename) { // Clear any previous data Clear(); int fileHandle = FileOpen(filename, FILE_READ|FILE_TXT); if(fileHandle == INVALID_HANDLE) { Print("Error opening file: ", filename, " err=", _LastError); return 0; } if(_hasHeader) { // read first line as header if(!FileIsEnding(fileHandle)) { string headerLine = FileReadString(fileHandle); string headerFields[]; int colCount = StringSplit(headerLine, StringGetCharacter(_separator,0), headerFields); for(int i=0; i<colCount; i++) Columns.Add(headerFields[i], i); } } uint rowCount=0; while(!FileIsEnding(fileHandle)) { string line = FileReadString(fileHandle); if(line == "") continue; // skip empty lines string fields[]; int fieldCount = StringSplit(line, StringGetCharacter(_separator,0), fields); if(fieldCount<1) continue; // no data? CArrayString *row = new CArrayString; for(int i=0; i<fieldCount; i++) row.Add(fields[i]); Rows.Add(row); rowCount++; } FileClose(fileHandle); return rowCount; }
Esta função Load():
- Limpa dados antigos.
- Abre o arquivo.
- Se _hasHeader for verdadeiro, lê a primeira linha como cabeçalho e preenche as colunas.
- Depois lê linhas até o fim do arquivo, dividindo-as em campos.
- Para cada linha, cria um CArrayString, preenche-o e o adiciona a Rows.
- Retorna o número de linhas de dados lidas.
Para unir tudo, agora temos boa parte da lógica definida. Nas próximas seções, refinaremos e finalizaremos o código, adicionaremos os métodos de acesso que faltam e mostraremos o código completo final. Também demonstraremos exemplos de uso, como verificar quantas linhas você obteve, quais colunas existem e como buscar valores de forma segura.
Ao percorrer esses trechos de código, você pode ver como as partes da lógica se encaixam. A classe final de leitura CSV será autossuficiente e fácil de integrar: basta criar uma instância, chamar Load("myfile.csv") e usar GetValueByName() ou GetValueByIndex() para obter as informações necessárias.
Na próxima seção, completaremos toda a implementação da classe e mostraremos um trecho final pronto para copiar e adaptar. Depois disso, finalizaremos com alguns exemplos de uso e considerações finais.
Finalizando a implementação da classe leitora de CSV
Nas seções anteriores, definimos a estrutura de nosso leitor CSV e trabalhamos em várias partes do código. Agora é hora de unir tudo em uma implementação única e coesa. Depois, mostraremos brevemente como utilizá-la. Na estrutura final do artigo, apresentaremos todo o código de uma só vez aqui, para que você tenha uma referência clara.
Integraremos as funções auxiliares discutidas — carregar arquivos, analisar cabeçalhos, armazenar linhas e métodos de acesso — em uma única classe MQL5. Em seguida, mostraremos um pequeno exemplo de como você pode usar a classe em seu EA ou script. Lembre-se de que esta classe:
- Lê um CSV do diretório MQL5/Files.
- Se _hasHeader for verdadeiro, ele extrai os nomes das colunas da primeira linha.
- As linhas subsequentes formam linhas de dados armazenadas em CArrayString.
- Você pode recuperar valores pelo nome da coluna (se houver cabeçalho) ou pelo índice da coluna.
Também incluiremos alguma verificação de erros e valores padrão. Vamos apresentar o código completo agora. Observe que este código é um exemplo ilustrativo e pode exigir pequenos ajustes dependendo do seu ambiente. Assumimos que os arquivos HashMap.mqh, ArrayString.mqh e ArrayObj.mqh estão disponíveis nos diretórios padrão de includes do MQL5.
Segue abaixo o código completo do leitor de CSV:
//+------------------------------------------------------------------+ //| CSimpleCSVReader.mqh | //| A simple CSV reader class in MQL5. | //| Assumes CSV file is located in MQL5/Files. | //| By default, uses ';' as the separator and treats first line as | //| header. If no header, columns are accessed by index only. | //+------------------------------------------------------------------+ #include <Generic\HashMap.mqh> #include <Arrays\ArrayObj.mqh> #include <Arrays\ArrayString.mqh> class CSimpleCSVReader { private: bool _hasHeader; string _separator; CHashMap<string,uint> Columns; CArrayObj Rows; // Array of CArrayString*, each representing a data row public: CSimpleCSVReader() { _hasHeader = true; _separator = ";"; } ~CSimpleCSVReader() { Clear(); } void SetHasHeader(bool hasHeader) {_hasHeader = hasHeader;} void SetSeparator(string sep) {_separator = sep;} // Load: Reads the file, returns number of data rows. uint Load(string filename); // GetValue by name or index: returns specified cell value or errorVal if not found string GetValueByName(uint rowNum, string colName, string errorVal=""); string GetValueByIndex(uint rowNum, uint colIndex, string errorVal=""); // Returns the number of data rows (excluding header) uint RowCount() {return Rows.Total();} // Returns the number of columns. If no header, returns column count of first data row uint ColumnCount() { if(Columns.Count() > 0) return Columns.Count(); // If no header, guess column count from first row if available if(Rows.Total()>0) { CArrayString *r = Rows.At(0); return (uint)r.Total(); } return 0; } // Get column name by index if header exists, otherwise return empty or errorVal string GetColumnName(uint colIndex, string errorVal="") { if(Columns.Count()==0) return errorVal; // Extract keys and values from Columns string keys[]; int vals[]; Columns.CopyTo(keys, vals); if(colIndex < (uint)ArraySize(keys)) return keys[colIndex]; return errorVal; } private: void Clear() { for(int i=0; i<Rows.Total(); i++) { CArrayString *row = Rows.At(i); if(row != NULL) delete row; } Rows.Clear(); Columns.Clear(); } }; //+------------------------------------------------------------------+ //| Implementation of Load() method | //+------------------------------------------------------------------+ uint CSimpleCSVReader::Load(string filename) { Clear(); // Start fresh int fileHandle = FileOpen(filename, FILE_READ|FILE_TXT); if(fileHandle == INVALID_HANDLE) { Print("CSVReader: Error opening file: ", filename, " err=", _LastError); return 0; } uint rowCount=0; // If hasHeader, read first line as header if(_hasHeader && !FileIsEnding(fileHandle)) { string headerLine = FileReadString(fileHandle); if(headerLine != "") { string headerFields[]; int colCount = StringSplit(headerLine, StringGetCharacter(_separator,0), headerFields); for(int i=0; i<colCount; i++) Columns.Add(headerFields[i], i); } } while(!FileIsEnding(fileHandle)) { string line = FileReadString(fileHandle); if(line == "") continue; // skip empty lines string fields[]; int fieldCount = StringSplit(line, StringGetCharacter(_separator,0), fields); if(fieldCount<1) continue; // no data? CArrayString *row = new CArrayString; for(int i=0; i<fieldCount; i++) row.Add(fields[i]); Rows.Add(row); rowCount++; } FileClose(fileHandle); return rowCount; } //+------------------------------------------------------------------+ //| GetValueByIndex Method | //+------------------------------------------------------------------+ string CSimpleCSVReader::GetValueByIndex(uint rowNum, uint colIndex, string errorVal="") { if(rowNum >= Rows.Total()) return errorVal; CArrayString *aRow = Rows.At(rowNum); if(aRow == NULL) return errorVal; if(colIndex >= (uint)aRow.Total()) return errorVal; string val = aRow.At(colIndex); return val; } //+------------------------------------------------------------------+ //| GetValueByName Method | //+------------------------------------------------------------------+ string CSimpleCSVReader::GetValueByName(uint rowNum, string colName, string errorVal="") { if(Columns.Count() == 0) { // No header, can't lookup by name return errorVal; } uint idx; bool found = Columns.TryGetValue(colName, idx); if(!found) return errorVal; return GetValueByIndex(rowNum, idx, errorVal); } //+------------------------------------------------------------------+
Vamos analisar mais de perto o Load(). Ele limpa os dados antigos, tenta abrir o arquivo e, se _hasHeader for verdadeiro, lê uma linha como cabeçalho. Depois divide e armazena os nomes das colunas. Em seguida, percorre o arquivo linha por linha, ignorando linhas vazias e dividindo as válidas em campos. Cada conjunto de campos se torna uma linha CArrayString em Rows. Ao final, você sabe exatamente quantas linhas obteve, e Columns está preparado para buscas por nome. Esse fluxo direto permite que seu EA se adapte facilmente se o CSV de amanhã tiver mais linhas ou formatação levemente diferente.
Sobre os métodos GetValueByName() e GetValueByIndex(). Esses métodos de acesso são sua interface principal com os dados. Eles são seguros porque sempre verificam os limites. Se você solicitar uma linha ou coluna que não existe, recebe um valor padrão inofensivo em vez de um crash. Se não houver cabeçalho, GetValueByName() retorna um valor de erro de forma adequada. Assim, mesmo que seu CSV esteja faltando algo ou _hasHeader esteja configurado incorretamente, seu EA não será interrompido. Você pode adicionar um Print() rápido se quiser registrar essas inconsistências, mas isso é opcional. O ponto é: esses métodos mantêm seu fluxo de trabalho suave e livre de erros.
Se params.csv for assim:
Symbol;MaxLot;MinSpread
EURUSD;0.20;1
GBPUSD;0.10;2
Saída:
Loaded 2 data rows. First Row: Symbol=EURUSD MaxLot=0.20 MinSpread=1
E se você quiser acessar por índice em vez de nome:
// Access second row, second column (MaxLot) by index: string val = csv.GetValueByIndex(1, 1, "N/A"); Print("Second row, second column:", val);
Isso deve imprimir 0.10, correspondente ao MaxLot do GBPUSD.
E se não houver cabeçalho? Se _hasHeader estiver definido como false, ignoramos a criação do mapa Columns. Então você deve usar GetValueByIndex() para acessar os dados. Por exemplo, se seu CSV não tiver cabeçalho e cada linha tiver três campos, você sabe:
- Coluna 0: Symbol
- Coluna 1: Price
- Coluna 2: Comment
Você pode chamar diretamente csv.GetValueByIndex(rowNum, 0) para obter o símbolo.
E quanto ao tratamento de erros? Nosso código retorna valores padrão se algo estiver faltando, como uma coluna ou linha inexistente. Também imprime erros se o arquivo não puder ser aberto. Na prática, você pode querer um registro mais robusto. Por exemplo, se você depende fortemente de dados externos, considere verificar rows = csv.Load("file.csv") e, se rows == 0, tratar isso adequadamente. Talvez você interrompa a inicialização do EA ou volte para padrões internos.
Não implementamos tratamento extremo para CSVs malformados ou codificações incomuns. Para cenários mais complexos, adicione verificações. Se ColumnCount() for zero, talvez registre um aviso. Se uma coluna necessária não existir, imprima uma mensagem na aba Experts.
Vamos analisar o desempenho: para arquivos CSV pequenos ou moderados, essa abordagem é perfeitamente adequada. Se você precisar lidar com arquivos extremamente grandes, considere estruturas de dados mais eficientes ou uma abordagem em streaming. No entanto, para uso típico em EAs — como ler algumas centenas ou milhares de linhas — isso terá desempenho adequado.
Agora temos um leitor CSV completo. Na próxima (e última) seção, discutiremos brevemente testes, apresentaremos alguns cenários de uso e concluiremos com observações finais. Você terá uma classe CSV pronta para uso, integrada de forma simples aos seus EAs ou scripts em MQL5.
Cenários de Testes e Uso
Com a implementação do leitor CSV concluída, é prudente confirmar que tudo funciona conforme o esperado. Testar é simples: crie um pequeno arquivo CSV, coloque-o em MQL5/Files e escreva um EA que o carregue e imprima alguns resultados. Você pode então checar a aba Experts para ver se os valores estão corretos. Aqui estão algumas sugestões de teste:
-
Teste Básico com Cabeçalho: Crie um test.csv como:
Symbol;Spread;Comment EURUSD;1;Major Pair USDJPY;2;Another Major
Carregue-o com:
CSimpleCSVReader csv; csv.SetHasHeader(true); csv.SetSeparator(";"); uint rows = csv.Load("test.csv"); Print("Rows loaded: ", rows); Print("EURUSD Spread: ", csv.GetValueByName(0, "Spread", "N/A")); Print("USDJPY Comment: ", csv.GetValueByName(1, "Comment", "N/A"));
Verifique a saída. Se mostrar “Rows loaded: 2”, “EURUSD Spread: 1” e “USDJPY Comment: Another Major”, então está funcionando.
E se o CSV não for perfeitamente uniforme? Suponha que uma linha tenha menos colunas do que o esperado. Nossa abordagem não força consistência. Se uma linha estiver faltando um campo, solicitar essa coluna retorna um valor padrão. Isso é bom se você consegue lidar com dados parciais, mas se precisar de formatação estrita, considere verificar as contagens de colunas após o Load() . Para arquivos enormes, esse método ainda funciona, embora se você estiver processando dezenas de milhares de linhas, talvez comece a pensar em otimizações de desempenho ou carregamento parcial. Para necessidades do dia a dia — CSVs pequenos a médios — essa configuração é mais do que suficiente.
-
Teste Sem Cabeçalho: Se você definir csv.SetHasHeader(false); e usar um arquivo sem cabeçalho:
EURUSD;1;Major Pair USDJPY;2;Another Major
Agora você deve acessar as colunas por índice:
string val = csv.GetValueByIndex(0, 0, "N/A"); // should be EURUSD Print("Row0 Col0: ", val);
Confirme que a saída corresponde às suas expectativas. - Colunas ou Linhas Ausentes: Tente solicitar um nome de coluna que não exista, ou uma linha além dos dados carregados. Você deve obter os valores de erro padrão que forneceu. Por exemplo:
string nonExistent = csv.GetValueByName(0, "NonExistentColumn", "MISSING"); Print("NonExistent: ", nonExistent);
Isso deve imprimir MISSING em vez de travar. - Arquivos Maiores: Se você tiver um arquivo com mais linhas, carregue-o e confirme a contagem de linhas. Verifique se o uso de memória e o desempenho permanecem razoáveis. Esta etapa ajuda a garantir que a abordagem seja robusta o suficiente para seu cenário.
Considere também codificações de caracteres e símbolos incomuns. A maioria dos CSVs é ASCII simples ou UTF-8, que o MQL5 lida bem. Se algum dia aparecerem caracteres estranhos, converter o arquivo para uma codificação mais adequada pode ajudar. Da mesma forma, se seu CSV tiver espaços em branco finais ou pontuação estranha, aparar os campos após a divisão garante dados mais limpos. Testar esses cenários “menos bonitos” agora garante que, quando seu EA rodar de verdade, ele não falhe por causa de um formato de arquivo ligeiramente diferente ou um glifo inesperado.
Cenários de Uso:
-
Parâmetros Externos:
Suponha que você tenha um CSV com parâmetros de estratégia. Cada linha pode definir um símbolo e alguns limites. Em vez de codificar esses valores no seu EA, você pode carregá-los na inicialização, iterar sobre as linhas e aplicá-los dinamicamente. Alterar parâmetros fica tão simples quanto editar o CSV, sem necessidade de recompilar. -
Gerenciamento de Watchlist:
Você pode armazenar uma lista de símbolos para operar em um arquivo CSV. Seu EA pode ler essa lista em tempo de execução, permitindo adicionar ou remover instrumentos rapidamente sem tocar no código. Por exemplo, um CSV pode conter:Symbol EURUSD GBPUSD XAUUSDLer este arquivo e iterar sobre as linhas no seu EA permite adaptar os símbolos negociados em tempo real. - Integração com Outras Ferramentas: Se você tem um script Python ou outra ferramenta gerando análises em CSV — como sinais customizados ou previsões — você pode exportar os dados para CSV e fazer com que seu EA os importe no MQL5. Isso cria uma ponte entre diferentes ecossistemas de programação.
Conclusão
Exploramos agora os fundamentos das operações de arquivos em MQL5, aprendemos a ler com segurança arquivos de texto linha a linha, a analisar linhas CSV em campos e a armazená-los para recuperação fácil por nomes de coluna ou índices. Ao apresentar o código completo de um leitor CSV simples, fornecemos um bloco de construção que pode melhorar suas estratégias automatizadas de negociação.
Essa classe leitor CSV não é apenas um trecho de código; é uma utilidade prática que você pode adaptar conforme necessário. Precisa de um delimitador diferente? Altere _separator. Sem cabeçalho no seu arquivo? Defina _hasHeader como false e confie nos índices. A abordagem é flexível e transparente, permitindo integrar dados externos de forma limpa. À medida que você desenvolve ideias de negociação mais complexas, pode estender esse leitor CSV — adicionando tratamento de erros mais robusto, suportando diferentes codificações ou até escrevendo de volta em arquivos CSV. Por enquanto, essa base deve cobrir a maioria dos cenários básicos.
Lembre-se: dados confiáveis são fundamentais para construir lógica de negociação sólida. Com a capacidade de importar dados externos de arquivos CSV, você pode aproveitar uma gama maior de insights de mercado, configurações e conjuntos de parâmetros, todos controlados dinamicamente por arquivos de texto simples em vez de valores codificados.Se suas necessidades crescerem — como lidar com múltiplos delimitadores, ignorar certas linhas ou suportar campos entre aspas — basta ajustar o código. Essa é a vantagem de ter seu próprio leitor CSV: é uma base sólida que você pode refinar conforme sua estratégia e fontes de dados evoluem. Com o tempo, você pode até construir um pequeno kit de ferramentas de dados em torno dele, sempre pronto para alimentar seu EA com novos insights sem reescrever a lógica central do zero.
Bom desenvolvimento e bons trades!
Traduzido do Inglês pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/en/articles/16614
Aviso: Todos os direitos sobre esses materiais pertencem à MetaQuotes Ltd. É proibida a reimpressão total ou parcial.
Esse artigo foi escrito por um usuário do site e reflete seu ponto de vista pessoal. A MetaQuotes Ltd. não se responsabiliza pela precisão das informações apresentadas nem pelas possíveis consequências decorrentes do uso das soluções, estratégias ou recomendações descritas.
Integre seu próprio LLM ao EA (Parte 5): Desenvolva e Teste Estratégia de Trading com LLMs (III) – Adapter-Tuning
Estratégia de trading "Captura de Liquidez" (Liquidity Grab)
Automatizando Estratégias de Trading em MQL5 (Parte 2): O Sistema Kumo Breakout com Ichimoku e Awesome Oscillator
Implementação do algoritmo criptográfico SHA-256 do zero em MQL5
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso