
Dados de mercado sem intermediários: conectando MetaTrader 5 à MOEX via ISS API
Cada pessoa tem seus próprios "desejos", tanto no sentido amplo quanto em nichos específicos, como no trading, se você tem interesse ou atua profissionalmente na área. O problema é que, na prática, conseguir o que se deseja, como um conjunto específico de instrumentos de negociação, o nível de serviço da corretora ou um terminal cliente conveniente — nem sempre é possível. Em especial, para residentes da Rússia, é difícil encontrar uma corretora que ofereça ao mesmo tempo acesso à Bolsa de Moscou e ao terminal MetaTrader 5. No mínimo, a oferta é limitada, o que obriga a buscar alternativas. Suponhamos que não estejamos dispostos a trocar o MetaTrader 5 por outro terminal, nesse caso o que nos resta?
Proponho utilizar os serviços web abertos da MOEX, que poderão ser facilmente... ou, ao menos, não tão dificilmente... integrados ao terminal após a leitura deste artigo. Aqui, falaremos sobre o serviço ISS — Informações e estatísticas da Bolsa de Moscou. Em termos técnicos, é uma combinação do protocolo HTTP com a tecnologia de serviços REST; de maneira prática, é algo que podemos consultar por meio de páginas web legíveis em um navegador comum ou via programas especialmente desenvolvidos para baixar e analisar os dados em formatos mais adequados para software, como xml, csv, json.
A Bolsa também oferece serviços mais avançados, que por isso são pagos, mas baseados nos mesmos princípios técnicos. Portanto, quem treinar com o ISS poderá depois aprimorar a solução para atender a um leque mais amplo de tarefas.
De forma geral, o ISS permite obter listas e especificações de instrumentos, cotações, ticks/negócios (do dia atual; históricos sob assinatura), dados estatísticos (como volumes e interesse aberto), books de ofertas em tempo real (sob assinatura), entre outros. Os dados gratuitos são fornecidos com 15 minutos de atraso, mas isso é totalmente suficiente para análise e identificação de sinais de trading intradiários com frequência inferior às estratégias HFT.
A lista das chamadas "pontos finais" (endpoints), que representam solicitações específicas (comandos) ao servidor, está apresentada abaixo. Embora nesta camada inicial (de todas as APIs da Bolsa) não existam comandos para envio de ordens e operações completas de trading, até mesmo as informações indicativas obtidas já possibilitam enriquecer consideravelmente as ferramentas de análise de mercado, oferecer uma visão mais completa do mercado e melhorar as decisões de trading.
Hoje em dia, os traders comuns realizam operações na bolsa, geralmente através do terminal da própria corretora (aplicativo web no navegador ou um aplicativo desktop independente), que não oferece recursos tão avançados quanto o MetaTrader 5, seja em termos de análise técnica (incluindo indicadores personalizados e uma vasta gama de ferramentas da codebase) ou de testes de estratégias com dados históricos. Os traders profissionais, provavelmente, preferirão integração via protocolos FIX, TWIME, Plaza II, entre outros também suportados pela MOEX, mas mesmo para eles, muitas vezes é mais simples validar ideias usando MQL5 do que tentar replicar totalmente os recursos técnicos e algoritmos já prontos e embutidos no MetaTrader 5 dentro dos seus próprios programas. É exatamente por isso que surge o interesse em integrar a ISS API da bolsa ao terminal.
O objetivo deste artigo é preparar uma biblioteca que reflita a ISS API dentro do ambiente MQL5 da forma mais natural possível, com a implementação de todas as particularidades técnicas necessárias (ocultas internamente), e ao mesmo tempo, com dicas lógicas e sintáticas visíveis para o programador de aplicações e, se necessário, para o usuário final. Claro que o ideal seria alcançar uma cobertura completa da ISS API, mas existem certos obstáculos nesse caminho, os quais serão detalhados mais adiante.
Dados brutos
A bolsa disponibiliza uma grande quantidade de documentação sobre o ISS: manual do desenvolvedor, exemplos de requisições, lista de pontos finais e o chamado índice — um diretório das entidades de informação e sua estrutura organizacional — na prática, uma árvore que iremos explorar aos poucos em detalhes, e da qual "brotam" como folhas as especificações dos instrumentos, relatórios, arquivos e, de modo geral, as regras de funcionamento da API (cada ramo tem suas próprias "leis").
As duas principais formas de representação da ISS API são: a lista de pontos finais e o índice. Se a primeira descreve as ações disponíveis, a segunda define os dados sobre os quais essas ações são realizadas.
Os pontos finais são logicamente divididos em diversos grupos funcionais, sendo os principais:
- acesso a dados online de negociação;
- acesso ao histórico;
- indicadores estatísticos;
- análises;
- arquivos de documentos;
O índice, por sua vez, inclui seções como:
- "engines", os maiores blocos de nível superior;
- "markets", que são elementos menores dentro de cada engine específico;
- modos de negociação ("boards") e grupos de modos de negociação ("boardgroups"), específicos de cada mercado;
- tipos, grupos e coleções de títulos financeiros negociados em diferentes modos;
Tudo isso será analisado com mais detalhes adiante. Por ora, é importante observar que os elementos do índice atuam como parâmetros ao chamar funções de pontos finais específicos.
Infelizmente, a lista de pontos finais está apresentada no site da bolsa como uma página web ramificada. A estrutura e o formato interno dos capítulos — páginas com a descrição dos parâmetros de solicitações específicas, que se abrem ao clicar nos links da lista — sugerem que essa organização foi gerada a partir de uma especificação no formato OpenAPI. Se tivéssemos acesso à especificação completa e atualizada do ISS em formato OpenAPI, isso facilitaria bastante a geração de código, pois o OpenAPI foi concebido justamente para descrever APIs de forma legível por máquinas, permitindo a criação automatizada de esboços de código tanto para programas clientes quanto para programas servidores que implementam a respectiva API. No momento da redação deste artigo, a especificação oficial da bolsa em OpenAPI não está publicada publicamente, por isso recorreremos aos recursos web disponíveis.
Na prática, a página HTML com a lista é um texto estruturado, do qual é relativamente simples extrair os pontos ativos com algum conhecimento de JavaScript. Por exemplo, basta abrir a página com a lista em qualquer navegador compatível com Chromium, pressionar Ctrl+Shift+I para entrar no modo desenvolvedor, ir até a aba Console ou, na aba Elements, apertar Esc (abrirá um console menor), e ali você poderá digitar comandos em JavaScript.
[...document.querySelectorAll('dt a')].map(e => e.innerText)
Essa expressão reunirá todos os pontos finais em um array, que salvaremos em um arquivo de texto, como o reference.json (anexado ao artigo), e utilizaremos como entrada para o gerador de código.
Extração da lista de pontos finais a partir da página web do diretório ISS
Cada ponto final é um caminho dentro do servidor https://iss.moex.com/, ou seja, ao unir o endereço do servidor com o endereço do ponto final, obtemos uma URL que pode ser inserida diretamente no navegador ou consultada por meio de WebRequest a partir do MQL5 — e, com isso, receberemos os dados desejados. Por exemplo, para acessar o índice das entidades raiz do servidor, basta usar o seguinte URL:
https://iss.moex.com/iss/index
Como veremos mais adiante, muitos endereços contêm palavras entre colchetes — esses são parâmetros que devem ser substituídos por identificadores dos objetos cujas informações nos interessam. Por exemplo, no caminho seguinte, é necessário especificar o nome de um engine [engine] e de um market [market] para obter a lista dos modos de negociação disponíveis nesses elementos.
https://iss.moex.com/iss/engines/[engine]/markets/[market]/boards
O servidor consegue fornecer dados em um dos seguintes formatos:
- HTML;
- XML;
- CSV;
- JSON;
Para especificar o formato, é necessário adicionar a extensão correspondente ao final do caminho da requisição, por exemplo,
https://iss.moex.com/iss/engines/[engine]/markets/[market]/boards.json
Se nenhuma extensão for especificada, o servidor, por padrão, retorna uma página HTML legível para humanos. No entanto, para análise algorítmica dos dados, é melhor optar por um formato mais estruturado, e por isso, usaremos o JSON — ele é mais leve que o XML, mais flexível que o CSV, e além disso, é ideal para programação orientada a objetos.
É importante destacar que a lista publicada está incompleta, contém imprecisões e erros de digitação, quando comparada com a realidade. Isso foi parcialmente corrigido no arquivo anexo. Também está incluída, para sua conveniência, a página web ISS Queries Reference.html, onde todas as endpoints estão descritas de forma "plana", ou seja, sem necessidade de clicar em links para ler suas descrições e parâmetros detalhados.
Segue abaixo a lista do diretório da API ISS, em ordem alfabética, com abreviações e agrupamento das principais categorias:
[
"/iss/analyticalproducts/curves/securities",
"/iss/analyticalproducts/curves/securities/[security]",
"/iss/analyticalproducts/futoi/securities",
"/iss/analyticalproducts/futoi/securities/[security]",
"/iss/archives/engines/[engine]/markets/[market]/[datatype]/[period]",
"/iss/archives/engines/[engine]/markets/[market]/[datatype]/years",
"/iss/archives/engines/[engine]/markets/[market]/[datatype]/years/[year]/months",
"/iss/engines",
"/iss/engines/[engine]",
"/iss/engines/[engine]/markets",
"/iss/engines/[engine]/markets/[market]",
"/iss/engines/[engine]/markets/[market]/boards",
"/iss/engines/[engine]/markets/[market]/boards/[board]",
"/iss/engines/[engine]/markets/[market]/boards/[board]/orderbook",
"/iss/engines/[engine]/markets/[market]/boards/[board]/securities",
"/iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]",
"/iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/candleborders",
"/iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/candles",
"/iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/orderbook",
"/iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/trades",
"/iss/engines/[engine]/markets/[market]/turnovers",
"/iss/engines/[engine]/markets/zcyc",
"/iss/engines/[engine]/turnovers",
"/iss/engines/[engine]/zcyc",
"/iss/events",
"/iss/events/[event_id]",
"/iss/history/engines/[engine]/markets/[market]/boards/[board]/dates",
"/iss/history/engines/[engine]/markets/[market]/boards/[board]/listing",
"/iss/history/engines/[engine]/markets/[market]/boards/[board]/securities",
"/iss/history/engines/[engine]/markets/[market]/boards/[board]/securities/[security]",
"/iss/history/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/dates",
"/iss/history/engines/[engine]/markets/[market]/boards/[board]/yields",
"/iss/history/engines/[engine]/markets/[market]/boards/[board]/yields/[security]",
"/iss/history/engines/[engine]/markets/[market]/dates",
"/iss/history/engines/[engine]/markets/[market]/listing",
"/iss/history/engines/[engine]/markets/[market]/securities",
"/iss/history/engines/[engine]/markets/[market]/securities/[security]",
"/iss/history/engines/[engine]/markets/[market]/securities/[security]/dates",
"/iss/history/engines/[engine]/markets/[market]/sessions",
"/iss/history/engines/[engine]/markets/[market]/sessions/[session]/securities",
"/iss/history/engines/[engine]/markets/[market]/sessions/[session]/securities/[security]",
"/iss/history/engines/[engine]/markets/[market]/yields",
"/iss/history/engines/[engine]/markets/[market]/yields/[security]",
"/iss/history/engines/stock/markets/shares/securities/changeover",
"/iss/history/engines/stock/totals/boards",
"/iss/history/engines/stock/totals/boards/[board]/securities",
"/iss/history/engines/stock/totals/boards/[board]/securities/[security]",
"/iss/history/engines/stock/totals/securities",
"/iss/history/engines/stock/zcyc",
"/iss/index",
"/iss/referencedata/engines/[engine]/markets/all/securitieslisting",
"/iss/referencedata/engines/futures/markets/[market]/params",
"/iss/referencedata/engines/futures/markets/[market]/risks",
"/iss/referencedata/engines/futures/markets/[market]/securities",
"/iss/referencedata/engines/stock/markets/all/securities",
"/iss/referencedata/engines/stock/markets/all/shorts",
"/iss/rms/engines/[engine]/objects/[object]",
"/iss/rms/engines/[engine]/objects/irr",
"/iss/rms/engines/[engine]/objects/irr/filters",
"/iss/rms/engines/[engine]/objects/settlementscalendar",
"/iss/sdfi/curves",
"/iss/sdfi/curves/[curveid]",
"/iss/sdfi/curves/securities",
"/iss/securities",
"/iss/securities/[security]",
"/iss/securities/[security]/aggregates",
"/iss/securities/[security]/dividends",
"/iss/securities/[security]/indices",
"/iss/securitygroups",
"/iss/securitygroups/[securitygroup]",
"/iss/securitygroups/[securitygroup]/collections",
"/iss/securitygroups/[securitygroup]/collections/[collection]",
"/iss/securitygroups/[securitygroup]/collections/[collection]/securities",
"/iss/sitenews",
"/iss/sitenews/[news_id]",
"/iss/statistics/complex/securities",
"/iss/statistics/complex/securities/[security]",
"/iss/statistics/engines/[engine]/derivatives/[report_name]",
"/iss/statistics/engines/[engine]/markets/[market]",
"/iss/statistics/engines/[engine]/markets/[market]/securities",
"/iss/statistics/engines/[engine]/markets/[market]/securities/[security]",
"/iss/statistics/engines/[engine]/monthly/[report_name]",
"/iss/statistics/engines/currency/markets/fixing",
"/iss/statistics/engines/currency/markets/fixing/[security]",
"/iss/statistics/engines/currency/markets/selt/rates",
"/iss/statistics/engines/futures/markets/[market]/openpositions",
"/iss/statistics/engines/futures/markets/[market]/openpositions/[asset]",
"/iss/statistics/engines/futures/markets/forts/series",
"/iss/statistics/engines/futures/markets/indicativerates/securities",
"/iss/statistics/engines/futures/markets/indicativerates/securities/[security]",
"/iss/statistics/engines/futures/markets/options/assets",
"/iss/statistics/engines/futures/markets/options/assets/[asset]",
"/iss/statistics/engines/futures/markets/options/assets/[asset]/openpositions",
"/iss/statistics/engines/futures/markets/options/assets/[asset]/optionboard",
"/iss/statistics/engines/futures/markets/options/assets/[asset]/turnovers",
"/iss/statistics/engines/futures/markets/options/assets/[asset]/volumes",
"/iss/statistics/engines/futures/markets/options/series",
"/iss/statistics/engines/futures/markets/options/series/[series_name]/securities",
"/iss/statistics/engines/state/markets/repo/cboper",
"/iss/statistics/engines/state/markets/repo/dealers",
"/iss/statistics/engines/state/markets/repo/mirp",
"/iss/statistics/engines/state/rates",
"/iss/statistics/engines/state/rates/columns",
"/iss/statistics/engines/stock/capitalization",
"/iss/statistics/engines/stock/currentprices",
"/iss/statistics/engines/stock/deviationcoeffs",
"/iss/statistics/engines/stock/markets/bonds/aggregates",
"/iss/statistics/engines/stock/markets/bonds/aggregates/columns",
"/iss/statistics/engines/stock/markets/bonds/monthendaccints",
"/iss/statistics/engines/stock/markets/index/analytics",
"/iss/statistics/engines/stock/markets/index/analytics/[indexid]",
"/iss/statistics/engines/stock/markets/index/analytics/[indexid]/tickers",
"/iss/statistics/engines/stock/markets/index/analytics/[indexid]/tickers/[ticker]",
"/iss/statistics/engines/stock/markets/index/analytics/columns",
"/iss/statistics/engines/stock/markets/index/bulletins",
"/iss/statistics/engines/stock/markets/index/rusfar",
"/iss/statistics/engines/stock/markets/shares/correlations",
"/iss/statistics/engines/stock/quotedsecurities",
"/iss/statistics/engines/stock/securitieslisting",
"/iss/statistics/engines/stock/splits",
"/iss/statistics/engines/stock/splits/[security]",
"/iss/turnovers",
"/iss/turnovers/columns",
]
Pelos nomes é fácil deduzir que algumas requisições são voltadas para o mercado de ações, outras para o mercado futuro, e ainda outras para títulos ou moedas. O significado de cada requisição pode ser compreendido pela última parte do caminho. Por exemplo, caminhos terminados em /securities retornam uma lista de instrumentos para o contexto correspondente (mercado, modo de negociação, sessão etc.). Caminhos com o parâmetro [security] ao final fazem requisições sobre um símbolo específico (especificações, dados atuais de negociação etc.). Os nomes dos caminhos com "sufixos" também são bastante intuitivos: /candles (velas), /orderbook (book de ofertas), /trades (negócios, ou seja, ticks), /turnovers (volumes negociados), /dates (intervalos de datas) e assim por diante.
Uma exceção nesta lista é o "/iss/index" — a requisição que retorna o índice geral do serviço. Esta chamada é, normalmente, o ponto de partida para qualquer novo programa ou serviço. Ao longo do dia, deve-se usar a versão salva localmente. Em geral, as alterações mais prováveis no índice estão relacionadas a opções que podem ser ativadas ou desativadas (0/1, desligado/ligado), que afetam determinados aspectos do funcionamento, enquanto os conjuntos de engines, mercados e modos de negociação permanecem estáveis por longos períodos. Essa característica permite refletir a estrutura organizacional do índice na hierarquia dos códigos-fonte dos sistemas voltados para a bolsa.
Vamos mostrar um pequeno trecho em formato JSON, para que você tenha uma ideia da estrutura do índice:
{
"engines": {
"columns": ["id", "name", "title"],
"data": [
[1, "stock", "Фондовый рынок и рынок депозитов"],
[2, "state", "Рынок ГЦБ (размещение)"],
[3, "currency", "Валютный рынок"],
[4, "futures", "Срочный рынок"],
[5, "commodity", "Товарный рынок"],
[6, "interventions", "Товарные интервенции"],
[7, "offboard", "ОТС-система"],
[9, "agro", "Агро"],
[1012, "otc", "ОТС с ЦК"],
[1282, "quotes", "Квоты"],
[1326, "money", "Денежный рынок"]
]
},
"markets": {
"columns": ["id", "trade_engine_id", "trade_engine_name", "trade_engine_title", "market_name", "market_title",
"market_id", "marketplace", "is_otc", "has_history_files", "has_history_trades_files", "has_trades", "has_history",
"has_candles", "has_orderbook", "has_tradingsession", "has_extra_yields", "has_delay"],
"data": [
[1328, 1326, "money", "Денежный рынок", "repo", "РЕПО ФК", 1328, "MONEY", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1327, 1326, "money", "Денежный рынок", "deposit", "Депозиты ФК", 1327, "MONEY", 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1341, 3, "currency", "Валютный рынок", "otcindices", "Внебиржевые индексы", 1341, "INDICES", 1, 0, 0, 1, 1, 1, 0, 0, 0, 0],
[5, 1, "stock", "Фондовый рынок и рынок депозитов", "index", "Индексы фондового рынка", 5, "INDICES", 0, 1, 0, 1, 1, 1, 0, 1, 0, 0],
[1, 1, "stock", "Фондовый рынок и рынок депозитов", "shares", "Рынок акций", 1, "MXSE", 0, 1, 1, 1, 1, 1, 1, 1, 0, 1],
[2, 1, "stock", "Фондовый рынок и рынок депозитов", "bonds", "Рынок облигаций", 2, "MXSE", 0, 1, 1, 1, 1, 1, 1, 1, 1, 1],
...
[33, 1, "stock", "Фондовый рынок и рынок депозитов", "moexboard", "MOEX Board", 33, null, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0],
[46, 1, "stock", "Фондовый рынок и рынок депозитов", "gcc", "РЕПО с ЦК с КСУ", 46, "MXSE", 0, 1, 1, 1, 1, 1, 1, 1, 0, 1],
[54, 1, "stock", "Фондовый рынок и рынок депозитов", "credit", "Рынок кредитов", 54, null, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1],
[10, 3, "currency", "Валютный рынок", "selt", "Биржевые сделки с ЦК", 10, "MXCX", 0, 1, 1, 1, 1, 1, 1, 0, 0, 1],
[34, 3, "currency", "Валютный рынок", "futures", "Поставочные фьючерсы", 34, "MXCX", 0, 1, 0, 1, 1, 1, 1, 0, 0, 1],
[41, 3, "currency", "Валютный рынок", "index", "Валютный фиксинг", 41, "FIXING", 0, 0, 0, 1, 1, 1, 0, 0, 0, 1],
[45, 3, "currency", "Валютный рынок", "otc", "Внебиржевой", 45, "MXCX", 0, 1, 1, 1, 1, 0, 0, 0, 0, 1],
[12, 4, "futures", "Срочный рынок", "main", "Срочные инструменты", 12, null, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1],
[22, 4, "futures", "Срочный рынок", "forts", "ФОРТС", 22, "FORTS", 0, 1, 1, 1, 1, 1, 1, 0, 0, 1],
[24, 4, "futures", "Срочный рынок", "options", "Опционы ФОРТС", 24, "OPTIONS", 0, 1, 1, 1, 1, 1, 1, 0, 0, 1],
...
[9, 2, "state", "Рынок ГЦБ (размещение)", "index", "Индексы ГКО\/ОФЗ", 9, null, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0],
[6, 2, "state", "Рынок ГЦБ (размещение)", "bonds", "Облигации ГЦБ", 6, null, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1],
[7, 2, "state", "Рынок ГЦБ (размещение)", "repo", "Междилерское РЕПО", 7, null, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1],
...
]
},
"boards": {
"columns": ["id", "board_group_id", "engine_id", "market_id", "boardid", "board_title", "is_traded", "has_candles", "is_primary"],
"data": [
[177, 57, 1, 1, "TQIF", "Т+: Паи - безадрес.", 1, 1, 1],
[178, 57, 1, 1, "TQTF", "Т+: ETF - безадрес.", 1, 1, 1],
[129, 57, 1, 1, "TQBR", "Т+: Акции и ДР - безадрес.", 1, 1, 1],
...
[135, 58, 1, 2, "TQOB", "Т+: Гособлигации - безадрес.", 1, 1, 1],
...
[44, 9, 1, 5, "SNDX", "Индексы фондового рынка", 1, 1, 1],
[102, 9, 1, 5, "RTSI", "Индексы РТС", 1, 1, 1],
[265, 104, 1, 5, "INAV", "INAV", 1, 1, 0],
...
[256, 88, 3, 34, "FUTS", "Фьючерсы системные - безадрес.", 0, 1, 1],
[257, 89, 3, 34, "FUTN", "Фьючерсы внесистемные- адрес.", 0, 1, 0],
[321, 165, 3, 41, "FIXI", "Валютный фиксинг", 1, 1, 1],
[411, 261, 3, 45, "OTCT", "Рынок OTC", 1, 0, 1],
...
[22, 15, 4, 12, "FOB", "Фьючерсы и Опционы", 0, 1, 1],
[101, 45, 4, 22, "RFUD", "Фьючерсы", 1, 1, 1],
[103, 35, 4, 24, "ROPD", "Опционы", 1, 1, 1],
[294, 138, 4, 37, "FIQS", "Фьючерсы IQS", 0, 0, 1],
[295, 139, 4, 38, "OIQS", "Опционы IQS", 0, 0, 1],
...
]
},
"boardgroups": {
...
},
"securitytypes": {
"columns": ["id", "trade_engine_id", "trade_engine_name", "trade_engine_title", "security_type_name",
"security_type_title", "security_group_name", "stock_type"],
"data": [
[3, 1, "stock", "Фондовый рынок и рынок депозитов", "common_share", "Акция обыкновенная", "stock_shares", "1"],
[1, 1, "stock", "Фондовый рынок и рынок депозитов", "preferred_share", "Акция привилегированная ", "stock_shares", "2"],
[51, 1, "stock", "Фондовый рынок и рынок депозитов", "depositary_receipt", "Депозитарная расписка", "stock_dr", "D"],
...
[7, 1, "stock", "Фондовый рынок и рынок депозитов", "public_ppif", "Пай открытого ПИФа", "stock_ppif", "9"],
[8, 1, "stock", "Фондовый рынок и рынок депозитов", "interval_ppif", "Пай интервального ПИФа", "stock_ppif", "A"],
[53, 1, "stock", "Фондовый рынок и рынок депозитов", "rts_index", "Индекс РТС", "stock_index", null],
...
[10, 2, "state", "Рынок ГЦБ (размещение)", "state_bond", "Государственная облигация", "stock_eurobond", null],
[1347, 3, "currency", "Валютный рынок", "currency_otcindices", "Валютные внебиржевые индексы", "currency_otcindices", null],
[75, 3, "currency", "Валютный рынок", "currency_index", "Валютный фиксинг", "currency_indices", null],
[5, 3, "currency", "Валютный рынок", "currency", "Валюта", "currency_selt", null],
[58, 3, "currency", "Валютный рынок", "gold_metal", "Металл золото", "currency_metal", null],
[59, 3, "currency", "Валютный рынок", "silver_metal", "Металл серебро", "currency_metal", null],
[62, 3, "currency", "Валютный рынок", "currency_futures", "Валютный фьючерс", "currency_futures", null],
[73, 3, "currency", "Валютный рынок", "currency_fixing", "Валютный фиксинг", "currency_selt", null],
...
[6, 4, "futures", "Срочный рынок", "futures", "Фьючерс", "futures_forts", null],
[52, 4, "futures", "Срочный рынок", "option", "Опцион", "futures_options", null],
...
]
},
"securitygroups": {
"columns": ["id", "name", "title", "is_hidden"],
"data": [
[12, "stock_index", "Индексы", 0],
[4, "stock_shares", "Акции", 0],
[3, "stock_bonds", "Облигации", 0],
[9, "currency_selt", "Валюта", 0],
[10, "futures_forts", "Фьючерсы", 0],
[26, "futures_options", "Опционы", 0],
[18, "stock_dr", "Депозитарные расписки", 0],
[33, "stock_foreign_shares", "Иностранные ц.б.", 0],
[6, "stock_eurobond", "Еврооблигации", 0],
[5, "stock_ppif", "Паи ПИФов", 0],
[20, "stock_etf", "Биржевые фонды", 0],
[24, "currency_metal", "Драгоценные металлы", 0],
[21, "stock_qnv", "Квал. инвесторы", 0],
[27, "stock_gcc", "Клиринговые сертификаты участия", 0],
[29, "stock_deposit", "Депозиты с ЦК", 0],
[28, "currency_futures", "Валютный фьючерс", 0],
[31, "currency_indices", "Валютные фиксинги", 0],
[1346, "currency_otcindices", "Валютные внебиржевые индексы", 0],
[22, "stock_mortgage", "Ипотечный сертификат", 1]
]
},
"securitycollections": {
"columns": ["id", "name", "title", "security_group_id"],
"data": [
[72, "stock_index_all", "Все индексы", 12],
[213, "stock_index_shares", "Основные индексы акций", 12],
[210, "stock_index_shares_sectoral", "Отраслевые индексы акций", 12],
...
[3, "stock_shares_all", "Все акции", 4],
[160, "stock_shares_one", "Уровень 1", 4],
[161, "stock_shares_two", "Уровень 2", 4],
[162, "stock_shares_three", "Уровень 3", 4],
[7, "stock_bonds_all", "Все", 3],
[163, "stock_bonds_one", "Все уровень 1", 3],
[164, "stock_bonds_two", "Все уровень 2", 3],
[165, "stock_bonds_three", "Все уровень 3", 3],
...
[227, "futures_forts_all", "Все фьючерсы", 10],
[226, "futures_forts_index", "Фьючерсы на индексы", 10],
[224, "futures_forts_shares", "Фьючерсы на акции", 10],
[225, "futures_forts_currency", "Фьючерсы на валюты", 10],
[228, "futures_forts_interest", "Фьючерсы на процентные ставки", 10],
[223, "futures_forts_commodity", "Фьючерсы на товарные контракты", 10],
...
]
}}
Embora o formato JSON não seja voltado à formatação de tabelas no sentido tradicional, os desenvolvedores da MOEX "empacotaram" as tabelas no JSON como arrays de arrays ("data") e adicionaram um array separado de cabeçalhos ("columns") com os nomes das colunas. Essa abordagem é usada em todas as respostas da ISS para o formato JSON.
É importante destacar que as tabelas de todas as entidades estão ligadas entre si por relações de "proprietário<->subordinado", estabelecidas por identificadores únicos (id). Por exemplo, os elementos da tabela "securitycollections" possuem, além do seu próprio identificador ("id"), um identificador do elemento pai "security_group_id", o qual deve ser buscado na coluna "id" da tabela relacionada "securitygroups".
Para melhor visualização, toda essa estrutura é apresentada no seguinte diagrama UML.
Diagrama UML composicional das entidades do ISS
No código-fonte do nosso programa, precisaremos descrever essas mesmas relações de alguma forma, a fim de verificar a correção das chamadas. As instruções correspondentes estão reunidas no arquivo moexdigest.mqh.
#define MOEX_COLUMNS_N 4 #define MOEX_COLUMN_ID 0 #define MOEX_COLUMN_NAME 1 #define MOEX_COLUMN_TITLE 2 #define MOEX_COLUMN_REF_ID 3 struct MoexColumn { string text; int index; }; struct MoexColumns { string entity; MoexColumn columns[MOEX_COLUMNS_N]; int relative; }; MoexColumns Digest[] = { {"engines", {{"id"}, {"name"}, {"title"}, {NULL}}, -1}, {"markets", {{"id"}, {"market_name"}, {"market_title"}, {"trade_engine_id"}}, 0}, // belongs to engine {"securitytypes", {{"id"}, {"security_type_name"}, {"security_type_title"}, {"trade_engine_id"}}, 0}, // belongs to engine {"securitygroups", {{"id"}, {"name"}, {"title"}, {"{engine_via_type}"}}, 0}, // belongs to engine transitively via security type {"boards", {{"id"}, {"boardid"}, {"board_title"}, {"market_id"}}, 1}, // belongs to market {"boardgroups", {{"id"}, {"name"}, {"title"}, {"market_id"}}, 1}, // belongs to market {"securitycollections", {{"id"}, {"name"}, {"title"}, {"security_group_id"}}, 3}, // belongs to security group {"sessions", {{"id"}, {"name"}, {"title"}, {NULL}}, 1} };
Cada linha da tabela Digest descreve um tipo específico de entidade do ISS. Além do tipo (campo entity), há um link para o objeto pai (campo relative, com o número da linha da entidade de nível superior). Cada nível possui 4 propriedades descritivas do tipo MoexColumn, extraídas das colunas nas tabelas do índice baixado. São importantes para nós o identificador, o nome curto, a descrição expandida e o nome da coluna que contém o identificador do elemento pai.
Por exemplo, na linha que começa com a palavra-chave "boards", vemos que o identificador, o nome e a descrição de uma "board" vêm das colunas: "id", "boardid", "board_title" (claro, essas colunas devem ser buscadas na tabela do índice chamada "boards"). O identificador do elemento pai ao qual essa "board" pertence está na coluna "market_id". Já o número de referência relative (1) indica que as informações sobre esse mercado (e todos os mercados) devem ser buscadas na linha do digest de número 1. E ali de fato lemos a palavra-chave "markets" e os dados equivalentes sobre as colunas com as propriedades dos mercados. Na primeira linha está a descrição de "engines", que não possuem um nível hierárquico superior, e por isso o campo relative é igual a -1.
A estrutura MoexColumn foi necessária para que, a partir do nome conhecido de uma coluna (campo text) de uma tabela específica, fosse possível descobrir de uma vez por todas o índice dessa coluna (campo index), e assim ler rapidamente os atributos ao analisar o índice.
Ao percorrer o índice conforme as regras do digest, conseguimos gerar quaisquer construções MQL5 baseadas nos elementos do MOEX ISS.
Mas que tipo de construções exatamente?
Conceito
A abordagem comum para resolver uma tarefa de programação com base em uma API envolve estudar a base necessária (estrutura da API) e partes específicas ou funções relacionadas à tarefa imediata. Nesse método, se for necessário realizar outra tarefa usando a mesma API, será preciso estudar novas seções e escrever código correspondente.
No contexto do trading, é difícil imaginar uma tarefa que não envolva acessar simultaneamente várias seções da API, e cada trader terá seu próprio conjunto de necessidades. Apesar de a ISS API ser uma API de nível básico (ou talvez justamente por seu caráter genérico), ela é bastante extensa. Assim, surge a questão: quais partes da API devem ser implementadas em MQL5 desde o início, e quais podem ser deixadas "para depois"?
Para evitar compromissos, decidiu-se oferecer suporte à API por completo (ou o mais próximo possível disso, pois existem obstáculos que discutiremos mais adiante), e resolver o problema da amplitude funcional de forma tecnológica e definitiva — com o uso de codificação automática.
A geração de código (codegen) é uma abordagem especial na programação, onde primeiro criamos outro programa que assumirá a tarefa de escrever o código-fonte principal. À primeira vista, isso pode parecer mais complicado. Mas, como ocorre frequentemente, a tarefa inicial tende a crescer em complexidade, e com essa estratégia conseguimos economizar muito trabalho repetitivo/tempo e eliminar erros humanos, como distrações ou erros de copiar e colar.
A geração de código se assemelha, de certa forma, ao modo como hoje em dia delegamos a criação de código-fonte a serviços como o ChatGPT e outros "geradores de conversa". A diferença é que nossa geração de código precisa ser rigorosa:
- deve estar em conformidade com a especificação da ISS API;
- deve produzir código limpo e correto em MQL5, que compile sem erros;
- deve ter uma arquitetura bem estruturada — modular e expansível;
- deve oferecer praticidade no uso (para ...);
Esses requisitos, até o momento, só podem ser plenamente atendidos se criarmos o gerador de código manualmente, sem apoio de IA. Na prática, uma ferramenta capaz de gerar código MQL5 com base na descrição OpenAPI do serviço facilitaria muito o processo ou até mesmo o automatizaria completamente. Contudo, como essa ferramenta ainda não existe, teremos que criar algo semelhante por conta própria.
Os três pontos no último item estão ali porque os usuários do produto podem ser tanto programadores MQL5 quanto traders comuns. Para cada um desses dois públicos, os critérios de usabilidade são diferentes. Por isso, os princípios de funcionamento e a arquitetura da biblioteca devem ser definidos desde o início. Hipoteticamente, seria possível oferecer múltiplas abordagens em uma única biblioteca, mas é melhor fazer isso de forma gradual e não logo na primeira versão.
A principal diferença ao projetar uma biblioteca voltada para programadores ou para usuários está no seguinte: Para programadores, é preferível que toda a API seja construída com tipagem rigorosa, com regras e parâmetros da especificação declarados diretamente no código-fonte. Isso permite escrever o código do novo produto com suporte imediato a sugestões inteligentes do editor, e garante a correção do programa ainda na fase de compilação, pois a sintaxe MQL5 da biblioteca se baseia na especificação da bolsa.
Contudo, essa abordagem dificulta a criação de programas em estilo universal — com possibilidade de adaptação para diferentes mercados e modos já no produto pronto. E para os usuários, justamente essa flexibilidade e possibilidade de configuração em tempo de uso são importantes. Para isso, o ideal é adotar uma tipagem mais livre internamente, ou seja, de forma simplificada, descrever objetos genéricos como "mercado" e "modo de negociação", e verificar se a combinação concreta entre mercado e modo está correta apenas durante a execução do programa.
Vamos demonstrar de forma esquemática a abordagem com tipagem rigorosa. Por exemplo, conhecendo a estrutura de hierarquia dos elementos do ISS já descritos, poderíamos projetar as seguintes classes:
namespace Moex { class Entity { public: virtual bool process() { /* делаем запрос и обработку данных */ return false; } // заглушка }; template<typename E> class Engine: public Entity { E engine; public: Engine(E e): engine(e) { } template<typename M> class Market: public Engine<E> { M market; public: Market(E e, M m): market(m), Engine<E>(e) { Bind(e, m); } template<typename B> class Board: public Market<M> { B board; public: Board(E e, M m, B b): board(b), Market<M>(e, m) { Bind(m, b); } }; }; }; };
Os templates seriam tipados com enumeradores, gerados com base no índice ISS. Assim, seria possível declarar no código classes específicas como:
// описываем объект для работы с нужным движком, рынком и режимом торгов Moex::Engine<MOEX_ENGINES>::Market<MOEX_MARKETS_STOCK>::Board<MOEX_BOARDS_STOCK_SHARES> b(stock, shares, TQBR); // выполняем запрос b.process();
Aqui, os identificadores stock, shares, TQBR pertencem a elementos dos enumeradores (enum) — respectivamente, MOEX_ENGINES, MOEX_MARKETS_STOCK, MOEX_BOARDS_STOCK_SHARES — e todos eles refletem com exatidão os vínculos definidos no índice, transferidos por um (hipotético) gerador de código para o MQL5.
Vale lembrar que os enumeradores de engines, mercados, "boards" e outras entidades só podem ser combinados conforme a hierarquia definida no índice. Em particular, os elementos de MOEX_MARKETS_STOCK só podem ser usados com o engine stock, como é fácil perceber pelo nome. Mas para o compilador esses nomes não significam nada, e por isso é necessário declarar de forma sintática as restrições impostas. No pseudocódigo acima, essa função é desempenhada pelas chamadas dos métodos template Bind, que o gerador de código criará apenas para os pares de tipos com combinações permitidas.
Se o programador tentar descrever um objeto com tipos incompatíveis (por exemplo, no exemplo citado, usar o mercado forts em vez de shares, sendo que o mercado forts pertence, na verdade, ao engine futures e não ao stock), a compilação resultará em erro.
Desse experimento teórico, podemos concluir que a abordagem com templates, embora forneça uma tipagem rigorosa, não é muito prática, pois a verificação ocorre apenas na fase de compilação, quando os objetos são instanciados. Além disso, a notação com templates acaba ficando excessivamente complexa.
Uma alternativa mais prática para programadores seria um gerador de código que criasse classes ou estruturas totalmente prontas com base no índice ISS. O volume de código gerado automaticamente aumentaria significativamente, mas isso eliminaria completamente os erros na aplicação, e o programador teria acesso imediato a sugestões inteligentes do editor, com listas suspensas de métodos ou propriedades válidas no contexto atual, à medida que digita as expressões.
Sugestões do MetaEditor no conjunto experimental de classes ISS com tipagem rigorosa
Infelizmente, o programa resultante estará "moldado" para uma tarefa específica, e a adaptação do comportamento com base nas variáveis de entrada será limitada (ou exigirá muita codificação repetitiva, para prever inúmeros cenários de uso — afinal, incorporar toda a funcionalidade da API ISS em um único programa se assemelha ao desenvolvimento de um terminal próprio).
Diante disso, foi tomada a decisão de implementar o gerador de código com uma abordagem flexível — utilizando o conjunto mais genérico possível de tipos: todos os mercados agrupados em um único enumerador (MOEX_MARKETS), todos os modos de negociação em outro (MOEX_BOARDS), e assim por diante — para cada seção raiz do índice ISS. A verificação da combinação correta dos elementos será feita em tempo de execução pelo usuário, mas o gerador de código se encarregará de criar esboços de funções para todas as possibilidades de verificação.
Mas antes, vamos definir o que deve ser gerado ao final pelo codificador automático.
Projeto
Como blocos de construção da biblioteca que será produzida pelo gerador de código, podemos utilizar: estruturas, classes, uniões e até funções isoladas. Testei diversas alternativas e, até o momento, optei por uma hierarquia relativamente óbvia de estruturas, que espelha a organização do diretório da API. Mais adiante, será possível retomar os experimentos com outras abordagens.
struct Iss: public Moex::Base { struct Analyticalproducts: public Moex::Base { } analyticalproducts; struct Archives: public Moex::Base { } archives; struct Engines: public Moex::Base { } engines; struct Events: public Moex::Base { } events; struct History: public Moex::Base { } history; struct Index: public Moex::Base { } index; struct Referencedata: public Moex::Base { } referencedata; struct Rms: public Moex::Base { } rms; struct Sdfi: public Moex::Base { } sdfi; struct Securities: public Moex::Base { } securities; struct Securitygroups: public Moex::Base { } securitygroups; struct Sitenews: public Moex::Base { } sitenews; struct Statistics: public Moex::Base { } statistics; struct Turnovers: public Moex::Base { } turnovers; } iss;
Todas as estruturas herdam de uma estrutura base chamada Base, onde reuniremos funções comuns. Cada estrutura interna recebe o nome do fragmento inicial distinto do caminho, e deve incluir métodos para acessar os pontos finais do grupo correspondente no diretório (como mostrado anteriormente).
Lembro que o caminho de cada ponto final pode conter parâmetros indicados entre colchetes — esses deverão ser passados como argumentos ao método. O nome do próprio método deve refletir a última parte do caminho, pois é ali que está o propósito da requisição. Por exemplo, na estrutura Engines, temos o seguinte ponto final:
/iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/candles
Como é fácil imaginar, ele serve para obter as velas ("candles") de um determinado símbolo (parâmetro "security") negociado no modo "board", do mercado "market", dentro do engine "engine". Portanto, vamos descrever dentro da estrutura um método candles com o conjunto correspondente de parâmetros:
Moex::Entity candles(string security, MOEX_BOARDS board, MOEX_MARKETS market = 0, MOEX_ENGINES engine = 0) { string templ = "/iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/candles"; Moex::Entity b = {}; ... return b; }
Por que invertemos a ordem dos parâmetros? Primeiro, porque em cada requisição o argumento mais importante é o último, se lermos pela ordem da especificação. No caminho acima, o último argumento é o nome do símbolo, como "GOLD", "AFLT" etc. É justamente a ação sobre o título financeiro específico que interessa ao usuário final, e não os meios técnicos para chegar até ele dentro da estrutura da bolsa. Com essa notação, o nome do método (a ação executada) e o nome do símbolo ao qual ela se refere ficam próximos na linha de código, o que facilita a leitura.
Segundo, nós já compreendemos a hierarquia das entidades da bolsa, e sabemos que os "boards" pertencem a mercados específicos, e os mercados pertencem a um engine específico. Assim, ao escolher o modo de negociação, podemos omitir os outros argumentos e deixar que o programa determine automaticamente o mercado e o engine corretos. E como sabemos pelas regras do MQL5, só é possível omitir os últimos elementos na lista de parâmetros de uma função.
O que é Moex::Entity? É uma estrutura placeholder (no namespace Moex), que será utilizada como valor de retorno padrão para cada função, sempre que não houver informações adicionais conhecidas sobre ela (explicaremos mais adiante de onde essas informações virão).
struct Entity: public Base { JsValue *_get(const string query = NULL) { return _fetch(_url + ".json" + link.buildQuery(query)); } }; struct Base { string _url; // конечная точка с подставленными параметрами ... static JsValue *_fetch(string url) { const static string host = "https://iss.moex.com"; // WebRequest(host + url) с помощью link const string text = link.fetch(host + url); const int code = link.getLastHttpCode(); if(code == 200) // успех { JsValue *obj = JsParser::jsonify(text); return obj; } else // обработка ошибок { ... } } static MoexLink link; };
Aqui está a implementação real da estrutura Entity e um pequeno trecho da estrutura Base. Entity representa a forma mais simples de processar uma requisição no método _get: ela baixa os dados no formato JSON para o endereço definido no campo _url, utilizando o método herdado _fetch. O campo _url está definido na estrutura pai Base, e esse campo deverá conter o caminho com os argumentos devidamente inseridos — essa é a função do método candles (e de cada outro método nas estruturas geradas). No momento, ele está representado por reticências, e logo vamos detalhá-lo.
A tarefa de download dos dados é executada diretamente por uma classe auxiliar chamada MoexLink (arquivo MoexLink.mqh) — não entraremos em detalhes sobre ela aqui, já que, como esperado, ela se baseia no WebRequest e nas regras de construção e tratamento de requisições via protocolo HTTP. O que merece destaque, porém, é que essa classe inclui modos específicos para salvar os dados recebidos da internet em arquivos locais no disco e permitir a leitura posterior desses dados a partir de cache, em vez de baixar tudo novamente em cada requisição — isso facilita a identificação de erros e permite reproduzir situações problemáticas durante o uso do depurador. Os dumps de dados são salvos/carregados a partir do diretório /MQL5/Files/MOEX/today (se existir), ou /MQL5/Files/MOEX/YYYYMMDD (para a data atual).
Além disso, a classe permite personalizar os cabeçalhos HTTP e definir parâmetros fixos que serão automaticamente incluídos em cada chamada ao WebRequest. Em particular, ao usar o ISS, vale a pena ativar a opção "iss.meta=off", para evitar o recebimento do bloco extra de dados descritivos — esse bloco precisa ser analisado apenas uma vez por tipo de requisição.
Voltando à estrutura Base. Os dados (texto) recebidos do serviço web são analisados em um objeto JSON e retornados ao código que os chamou. O parser utilizado é o ToyJson (arquivo toyjson2.mqh) — uma versão aprimorada daquele originalmente publicado no livro sobre algotrading.
Dessa forma, o método candles (e outros métodos aplicáveis) deverão seguir um modelo semelhante ao seguinte.
Moex::Entity candles(string security, MOEX_BOARDS board, MOEX_MARKETS market = 0, MOEX_ENGINES engine = 0) { string templ = "/iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/candles"; Moex::Entity b = {}; if(!(market = Moex::GetRelation(market, board))) { return b; } // ошибка - несоответствие рынка и доски if(!(engine = Moex::GetRelation(engine, market))) { return b; } // ошибка - несоответствие движка и рынка MOEX_REPE(templ, engine); MOEX_REPE(templ, market); MOEX_REPE(templ, board); MOEX_REPS(templ, security); b._url = templ; // сохраняем шаблон запроса с уже подставленными аргументами в пути return b; }
A função GetRelation deve ser gerada para verificar se os dois argumentos fornecidos (elementos dos enumeradores, que também são gerados automaticamente) são compatíveis entre si. No exemplo, verifica-se se o board pertence ao market e se o market pertence ao engine. Caso essa combinação não seja válida, deve ser emitida uma mensagem de erro e retornada uma estrutura vazia.
Os macros MOEX_REPx fazem a substituição dos valores reais dos argumentos na string templ nos locais onde estão os parâmetros da requisição.
#define MOEX_IP(S,C,P) StringReplace(S,"[" + #P + "]",C(P)) #define MOEX_REPS(S,P) MOEX_IP(S, string, P) #define MOEX_REPE(S,P) MOEX_IP(S, EnumToMOEXString, P)
Todos esses elementos fundamentais (classes-base, macros, etc.), nos quais se apoiam os códigos gerados, estão reunidos no arquivo moexcore.mqh, criado manualmente.
Em particular, nesse arquivo está declarado o enumerador MOEX_INTERVAL, que representa os timeframes suportados pelo ISS:
enum MOEX_INTERVAL { M1 = 1, M10 = 10, M60 = 60, H24 = 24, D7 = 7, D31 = 31, Q4 = 4 // NB: квартальный таймфрейм - отсутствует в MQL5! };
Vale observar que, no índice — trecho mostrado abaixo — eles são descritos sob a chave "durations", mas nos parâmetros das requisições da API são chamados de "interval".
"durations": { "columns": ["interval", "duration", "days", "title", "hint"], "data": [ [1, 60, null, "минута", "1м"], [10, 600, null, "10 минут", "10м"], [60, 3600, null, "час", "1ч"], [24, 86400, null, "день", "1д"], [7, 604800, null, "неделя", "1н"], [31, 2678400, null, "месяц", "1М"], [4, 8035200, null, "квартал", "1К"] ] },
Naturalmente, será necessário escrever uma função para converter esses intervalos nos timeframes nativos do MQL5 (exceto o trimestral, para o qual não há equivalente).
Adiantando um pouco, vale mencionar que a estrutura base Base também oferece suporte para download paginado de dados em caso de requisições volumosas, cache de objetos JSON alocados dinamicamente (para posterior coleta de lixo, sob demanda), além da verificação da resposta quanto à conformidade com os dados esperados. No entanto, este último item só será implementado mais adiante.
A identificação das entidades de diferentes tipos do ISS é convenientemente realizada com a ajuda dos enumeradores gerados, como MOEX_ENGINES, MOEX_MARKETS e assim por diante. Todos eles são gerados com base no índice e seguem o seguinte modelo (aqui com o exemplo de MOEX_ENGINES):
enum MOEX_ENGINES { no_engines = 0, // --//-- agro = 9, // (agro) Агро commodity = 5, // (commodity) Товарный рынок currency = 3, // (currency) Валютный рынок futures = 4, // (futures) Срочный рынок interventions = 6, // (interventions) Товарные интервенции money = 1326, // (money) Денежный рынок offboard = 7, // (offboard) ОТС-система otc = 1012, // (otc) ОТС с ЦК quotes = 1282, // (quotes) Квоты state = 2, // (state) Рынок ГЦБ (размещение) stock = 1, // (stock) Фондовый рынок и рынок депозитов };
Se você comparar essa definição com a respectiva seção do índice mostrada anteriormente, notará que há uma correspondência exata entre os nomes, valores e comentários com os nomes originais, identificadores e descrições.
Sabendo pelas regras do índice quais combinações de identificadores são válidas, é possível gerar funções para verificar a validade dessas combinações. Para isso, basta ao gerador de código criar arrays como, por exemplo:
MOEX_ENGINES GetRelation(MOEX_ENGINES pp, MOEX_MARKETS cc) { static const int ref[][2] = // MARKETS -> ENGINES { {1328, 1326}, {1327, 1326}, {1341, 3}, ... {23, 1}, {25, 1}, }; return Moex::CheckRelation(pp, cc, ref); }
Neste caso, é utilizada uma função genérica chamada CheckRelation, que verifica se o par [pp, cc] está presente no array ref. Essa função também está incluída no arquivo moexcore.mqh. Se o parâmetro da esquerda (o de nível superior) for igual a 0 (o que indica um argumento opcional omitido na chamada), a função seleciona automaticamente e retorna o identificador correto (elemento do enumerador apropriado).
OpenAPI
Até agora, analisamos os caminhos das endpoints como se fossem uma descrição completa das funções oferecidas pelo serviço web. Na verdade, não é bem assim. Uma requisição HTTP padrão inclui não apenas o endereço do servidor e o caminho correspondente, mas também os chamados parâmetros da string de consulta — que são definidos no URL após o sinal de interrogação '?'. Por exemplo,
https://iss.moex.com/iss/engines/stock/markets/shares/securities/AFLT/candles.json?from=2023-01-01&till=2024-01-01&interval=24
À esquerda do símbolo '?', temos o caminho, onde — como sabemos — podem aparecer segmentos variáveis (na especificação acima marcados entre colchetes), que chamamos de parâmetros de caminho. Esses parâmetros determinam a qual objeto no servidor estamos nos referindo e que tipo de informação queremos obter sobre ele.
À direita do '?', estão os pares nomeados "chave=valor", separados pelo caractere '&'. Esses parâmetros especificam exatamente como, em qual quantidade e em qual formato essas informações devem ser transmitidas.
No exemplo mostrado da consulta de candles, é indicado o ticker "AFLT", do mercado de ações, para o qual também foi especificado um intervalo de datas e o timeframe D1 das velas.
Anteriormente, apresentamos o link para a especificação da API ISS (https://iss.moex.com/iss/reference/) — ele leva à lista de pontos finais, que importamos para o arquivo reference.json. Cada ponto final possui sua própria lista de parâmetros para a string de consulta, e para visualizá-los é necessário clicar no link correspondente na página principal. Você também pode utilizar o arquivo ISS Queries Reference.html fornecido, onde todas as informações foram reunidas em um único lugar.
O gerador de código deve ser capaz de incluir na requisição não apenas os parâmetros de caminho (já descritos anteriormente), mas também os parâmetros da string de consulta — que são específicos para cada tipo de requisição.
A página web disponível é muito adequada para leitura por humanos, mas extrair dela informações estruturadas, que possam ser utilizadas pelo gerador de código, é uma tarefa bastante trabalhosa. Por isso, seria ideal ter a descrição da API em formato OpenAPI. Felizmente, foi possível encontrar na internet — mais precisamente no GitHub, uma versão não oficial do OpenAPI para o ISS. Embora esteja desatualizada e incompleta, ela pode ser adaptada. O arquivo de origem da especificação, convertido de YAML para JSON, acompanha o artigo com o nome moex-iss-api.json.
Não entraremos em detalhes sobre o formato OpenAPI. Basta dizer que, no nível superior, o documento possui as seguintes seções (chaves JSON):
- "openapi" — versão da especificação OpenAPI (existem várias, pois o padrão está em constante evolução);
- "info" — informações sobre o serviço web descrito neste arquivo específico;
- "servers" — endereço dos servidores ativos do serviço, como é o caso do https://iss.moex.com/ no ISS;
- "paths" — os pontos finais do serviço;
- "security" — método de autenticação (irrelevante no nosso caso, já que estamos lidando com a parte pública do ISS);
- "components" — todas as informações adicionais que não se encaixam nas seções anteriores; aqui é comum descrever as estruturas dos dados de entrada e saída das requisições;
Nosso foco principal está na seção "paths".
Vamos examinar seu conteúdo e ver como está descrito o trecho referente ao exemplo da requisição de velas mostrado anteriormente.
"/iss/engines/{engine}/markets/{market}/securities/{security}/candles.json" :
{
"get" :
{
"description" : "Получить свечи указанного инструмента по дефолтной группе режимов.",
"parameters" :
[
{
"name" : "engine",
"in" : "path",
"required" : true,
"schema" :
{
"type" : "string"
}
},
{
"name" : "market",
"in" : "path",
"required" : true,
"schema" :
{
"type" : "string"
}
},
{
"name" : "security",
"in" : "path",
"required" : true,
"schema" :
{
"type" : "string"
}
},
{
"name" : "candles.start",
"in" : "query",
"description" : "Номер строки (отсчет с нуля), с которой следует начать порцию возвращаемых данных (см. рук-во разработчика). Получение ответа без данных означает, что указанное значение превышает число строк, возвращаемых запросом.",
"required" : false,
"schema" :
{
"type" : "string",
"nullable" : true
}
},
{
"name" : "candles.till",
"in" : "query",
"description" : "Дата, до которой выводить данные. Формат: ГГГГ-ММ-ДД.",
"required" : false,
"schema" :
{
"type" : "string",
"nullable" : true
}
},
{
"name" : "candles.from",
"in" : "query",
"description" : "Дата, начиная с которой необходимо начать выводить данные. Формат: ГГГГ-ММ-ДД.",
"required" : false,
"schema" :
{
"type" : "string",
"nullable" : true
}
},
{
"name" : "candles.interval",
"in" : "query",
"description" : "Интервал графика.",
"required" : false,
"schema" :
{
"type" : "string",
"nullable" : true
}
},
{
"name" : "candles.iss.reverse",
"in" : "query",
"description" : "Изменить порядок сортировки на обратный. Принимает значения true/false.",
"required" : false,
"schema" :
{
"type" : "string",
"nullable" : true
}
}
],
}
},
O array "parameters" contém todos os parâmetros da requisição, e sua localização é indicada pela propriedade "in", que pode ter os valores "path" e "query" (existem outros, mas não nos interessam aqui). Os parâmetros de caminho ("path") podem ser extraídos diretamente do próprio caminho, mas os parâmetros da string de consulta ("query") só aparecem aqui. No caso da requisição de candles, ela oferece suporte aos seguintes parâmetros:
- candles.start — para navegação iterativa em grandes conjuntos de velas;
- candles.from — data inicial do intervalo desejado;
- candles.till — data final do intervalo desejado;
- candles.interval — o timeframe;
- candles.iss.reverse — ordem de ordenação das velas, direta ou reversa;
Além disso, para cada parâmetro também é indicado o tipo de dado — no campo "schema". Infelizmente, nesta versão da especificação, todos os parâmetros estão definidos como do tipo "string", o que impede uma tipagem rigorosa e dificulta a validação dos valores tanto por parte do programador quanto do usuário durante a montagem da requisição. Essa validação é essencial para o correto formatação dos parâmetros; sem ela, as requisições enviadas não serão processadas pelo servidor como esperado. Isso precisa ser corrigido.
Para ajustar automaticamente a especificação, foi criado um script auxiliar chamado moexOpenAPIedit.mq5. Sua função é atribuir os tipos de dados corretos a todos os parâmetros de todas as requisições. E de onde obtemos essa informação? A partir da análise "manual" da descrição textual — ao examinar atentamente o diretório da API, percebe-se que o conjunto de parâmetros é relativamente pequeno, com poucas dezenas de opções, o que permite agrupá-los por padrão: nomes usados para inteiros, para datas, e assim por diante. No entanto, isso significa que o script só será aplicável ao ISS e apenas a versões compatíveis do OpenAPI.
O script moexOpenAPIedit.mq5 possui um único parâmetro de entrada, OpenAPIjson, que serve para especificar o arquivo de especificação a ser editado. Durante a execução, serão gerados dois arquivos JSON: um com a especificação original e outro com a modificada — ambos no mesmo estilo de formatação, o que facilita a comparação visual de mudanças. O arquivo final com a especificação corrigida acompanha o artigo com o nome moex-iss-api-mod.json.
Vamos descrever, em linhas gerais, como esse arquivo foi obtido e qual o seu papel no processo seguinte de geração de código.
É evidente que os parâmetros das requisições HTTP implicam um tipo específico de dado. Por exemplo, parâmetros que definem intervalos numéricos devem ser do tipo integer, parâmetros que definem datas e horários — tipo date (ou date-time), flags booleanas — tipo boolean, e assim por diante (aqui usamos os nomes dos tipos segundo a notação OpenAPI, e ao convertê-los para MQL5 usaremos os tipos equivalentes — long, datetime, bool).
Vale observar que, por razões históricas, no ISS existem dois tipos de flags booleanas: as que esperam os valores true/false e as que esperam números inteiros 1/0. Esses dois tipos precisam ser diferenciados na especificação editada — para o primeiro tipo usaremos boolean, já para o segundo incluiremos no "schema" a propriedade adicional "format" : "int8".
Além disso, como planejamos gerar um enumerador específico para cada entidade do ISS (por exemplo, MOEX_ENGINES, MOEX_MARKETS etc.), podemos declarar esses tipos com mais rigor no OpenAPI usando a propriedade não padrão "x-enum-type", que será compreendida apenas pelo nosso gerador de código. Por exemplo, ao enviar como parâmetro uma das sessões de negociação, escreveremos dentro de "schema" — "x-enum-type" : "MOEX_SESSIONS", onde MOEX_SESSIONS é um enumerador gerado pelo codificador (curiosamente, a seção de sessões nem sequer aparece no arquivo de índice!).
Graças à descrição detalhada e legível por máquina dos parâmetros no formato JSON, o gerador de código passa a ter a capacidade de retornar, nos métodos, não mais uma estrutura "amorfa" como Moex::Entity, mas sim uma estrutura de tipo específico, adequada à natureza da requisição correspondente. Por exemplo, para a requisição de candles que examinamos anteriormente, o gerador criará uma estrutura chamada Candles (o exemplo mostrado é real):
#include "../moexcore.mqh" // (1) /iss/engines/[engine]/markets/[market]/boardgroups/[boardgroup]/securities/[security]/candles // Получить свечи указанного инструмента по выбранной группе режимов торгов. // (2) /iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/candles // Получить свечи указанного инструмента по выбранному режиму торгов. // (3) /iss/engines/[engine]/markets/[market]/securities/[security]/candles // Получить свечи указанного инструмента по дефолтной группе режимов. struct Candles: public Moex::Base { // (1)(2)(3) Номер строки (отсчет с нуля), с которой следует начать порцию возвращаемых данных (см. рук-во разработчика). // Получение ответа без данных означает, что указанное значение превышает число строк, возвращаемых запросом. long start; Candles _start(long s) { start = s; return this; } // (1)(2)(3) Дата, до которой выводить данные. Формат: ГГГГ-ММ-ДД. datetime till; Candles _till(datetime t) { till = t; return this; } // (1)(2)(3) Дата, начиная с которой необходимо начать выводить данные. Формат: ГГГГ-ММ-ДД. datetime from; Candles _from(datetime f) { from = f; return this; } // (1)(2)(3) Интервал графика. MOEX_INTERVAL interval; Candles _interval(MOEX_INTERVAL i) { interval = i; return this; } // (1)(2)(3) Изменить порядок сортировки на обратный. Принимает значения true/false. bool iss_reverse; Candles _iss_reverse(bool i) { iss_reverse = i; return this; } ... public: JsValue *_get(const string custom = NULL) { if(StringLen(_url) == 0) return (JsValue *)&JsValue::null; string query[]; if((start)) { ArrayFindOrInsert(query, MOEX_STRING(start)); } if((till)) { ArrayFindOrInsert(query, MOEX_STRING_DATE(till)); } if((from)) { ArrayFindOrInsert(query, MOEX_STRING_DATE(from)); } if((interval)) { ArrayFindOrInsert(query, MOEX_STRING(interval)); } if(iss_reverse) { ArrayFindOrInsert(query, "iss.reverse=" + (string)iss_reverse); } if(StringLen(custom)) ArrayFindOrInsert(query, custom); JsValue *obj = _fetch(_url + ".json" + link.buildQuery(StringCombine(query, '&'))); ... return obj; } };
É possível ver claramente como as descrições dos parâmetros da requisição, extraídas do OpenAPI, foram incorporadas quase sem alterações na definição da estrutura, inclusive com os comentários. Para cada parâmetro, existe uma variável-membro e um método setter de mesmo nome (com prefixo de sublinhado). Note que os tipos das variáveis impõem rigorosamente os dados esperados por parte do programador e do usuário. Todos os métodos setter retornam uma cópia da própria estrutura (this), o que permite encadear chamadas para configurar vários argumentos de uma vez.
Por fim, no método _get — que envia a requisição ao serviço web — todos os parâmetros não vazios são reunidos no array query como strings devidamente formatadas (de acordo com os tipos dos parâmetros, graças aos macros MOEX_STRING_XYZ definidos no moexcore.mqh). A função ArrayFindOrInsert adiciona os argumentos ao array em ordem alfabética, garantindo uma forma canônica para cada requisição — o que é importante para o mecanismo de cache.
Cada descrição de estrutura específica como essa costuma ser reutilizada por diversos pontos finais — esses usos são listados numericamente no cabeçalho dos comentários da estrutura. Cada estrutura gerada é salva em um arquivo separado com extensão ".inc", dentro da pasta MQL5/Files/MOEX/inc, e incluída via #include sempre que necessário nas estruturas de nível superior:
struct Iss: public Moex::Base { struct Analyticalproducts: public Moex::Base { ... } analyticalproducts; struct Archives: public Moex::Base { ... } archives; struct Engines: public Moex::Base { ... #include "/inc/EnginesCandles.inc" ... Candles candles(string security, MOEX_BOARDS board, MOEX_MARKETS market = 0, MOEX_ENGINES engine = 0) { string templ = "/iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/candles"; Candles b = {}; if(!(market = Moex::GetRelation(market, board))) { return b; } if(!(engine = Moex::GetRelation(engine, market))) { return b; } MOEX_REPE(templ, engine); MOEX_REPE(templ, market); MOEX_REPE(templ, board); MOEX_REPS(templ, security); b._url = templ; return b; } ... } engines; ... } iss;
Observe que, agora, o método candles retorna uma estrutura do tipo Candles.
Como resultado, o usuário da biblioteca poderá configurar e executar uma requisição de candles no seu próprio código da seguinte forma:
JsValue *data = iss.engines.candles(Ticker, Board, Market, Engine)._interval(Timeframe)._from(From)._till(To)._start(MOEX_ALL)._get();
Ou de maneira mais curta (porque nossa biblioteca consegue determinar automaticamente o mercado e o engine a partir do modo de negociação):
JsValue *data = iss.engines.candles(Ticker, Board)._interval(Timeframe)._from(From)._till(To)._start(MOEX_ALL)._get();
E, levando essa ideia adiante, seria possível extrair automaticamente o modo de negociação principal a partir da especificação do instrumento.
A constante MOEX_ALL, definida em moexcore.mqh como (-1), instrui a estrutura Base a fazer o download de todas as páginas de dados, caso o volume seja grande.
Retomando o trecho da especificação OpenAPI mostrado anteriormente, vale notar que alguns parâmetros trazem o prefixo "candles.". Esse prefixo indica o nome do bloco de dados na resposta do servidor ao qual esses parâmetros se referem. No caso, a requisição de candles retornará apenas um bloco de dados com as velas, mas há requisições que resultam em múltiplos blocos. Por exemplo, ao solicitar informações de um ticker, geralmente recebemos os blocos "securities" (com a descrição do símbolo), "marketdata" (com os dados mais recentes de negociação) e "dataversion" (com identificadores da versão do snapshot dos dados). Em teoria, existe um modo de excluir certos blocos da resposta ou solicitar apenas um bloco específico, com o objetivo de poupar recursos, mas o que nos interessa agora é outro ponto.
A presença desses prefixos possibilita implementar no gerador de código uma verificação da validade dos dados recebidos. Por exemplo, se enviarmos uma requisição de candles e a resposta JSON não contiver o bloco "candles", então houve algum erro na chamada. As linhas responsáveis por essas verificações são geradas nas estruturas específicas, como Candles — no exemplo anterior, foram omitidas por simplicidade e substituídas por reticências. Os interessados podem consultar os detalhes nos códigos-fonte fornecidos com o artigo.
Como a especificação OpenAPI que temos em mãos está incompleta, o gerador de código poderá gerar estruturas específicas apenas para parte dos pontos finais. Para os casos em que os respectivos modelos não estejam definidos, será inserido um comentário de aviso no código-fonte, e os métodos que realizam as requisições continuarão retornando a estrutura genérica Moex::Entity.
Implementação do gerador de código
Temos, então, os seguintes dados para alimentar o gerador de código:
- o índice das entidades de nível superior do ISS, obtido pela requisição /iss/index (index.json);
- o diretório de requisições do ISS, resultante da conversão da documentação oficial do ISS (reference.json);
- a especificação não oficial e incompleta do OpenAPI, que complementa o diretório (moex-iss-api-mod.json);
Todos esses arquivos podem ser carregados no gerador de código a partir da internet ou de um disco local (os nomes dos arquivos fornecidos com o artigo estão entre parênteses), mas é importante lembrar que apenas o índice é disponibilizado diretamente pelo serviço web; os outros dois arquivos de configuração, se desejado, devem ser preparados e hospedados por conta própria em algum servidor — dessa forma, será possível ajustar remotamente (ainda que em grau limitado) o comportamento do gerador de código conforme mudanças futuras no serviço.
O script do gerador de código chama-se moexindexer.mq5. Os dados mencionados são passados por meio de parâmetros input apropriados. Todos os arquivos são lidos e salvos, por padrão, no subdiretório MOEX.
input string MoexIssIndex = "MOEX/index.json"; input string MoexIssAPIReference = "MOEX/reference.json"; input string OpenAPIjson = "MOEX/moex-iss-api-mod.json";
O script processa separadamente o índice e, de forma independente, o diretório de requisições junto da especificação opcional em formato OpenAPI (caso ela não esteja disponível, será necessário descobrir manualmente a sintaxe de todas as requisições e montá-las como uma string única, sem qualquer proteção contra preenchimento incorreto).
void OnStart()
{
ConvertIndex();
ConvertReference();
}
Ambos os ramos funcionam segundo o mesmo princípio — carregamento do arquivo de origem, geração automática de código com base nesse conteúdo e gravação do resultado em um arquivo `.mqh`, que depois deve ser incluído no seu projeto de integração com a bolsa. Em particular, o resultado da conversão do índice é salvo no arquivo moexindex.mqh (caso o script seja executado sob o depurador, o nome do arquivo será moexinder-YYYYMMDDHHMM.mqh).
void ConvertIndex() { string data = GetData(MoexIssIndex); string header = MoexIndex2MQL5Converter(data); if(StringLen(header)) { const string filename = StringFormat("MOEX/moexindex%s.mqh", TIMESTAMP); if(WriteTextFile(filename, header)) { if(Logging) PrintFormat("File saved: %s", filename); } } }
Na função MoexIndex2MQL5Converter (aqui apresentada com simplificações), além da conversão esperada do texto para JSON, vemos também o processamento do digest mencionado anteriormente, para identificar os índices das colunas que estabelecem relações entre as entidades do índice.
string MoexIndex2MQL5Converter(const string text) { JsParser parser; JsValue *obj = parser.parse(text); if(!UpgradeIndex(obj)) return NULL; for(int k = 0; k < ArrayRange(Digest, 0); k++) { for(int i = 0; i < MOEX_COLUMNS_N; i++) { const int c = obj[Digest[k].entity]["columns"].indexOf(Digest[k].columns[i].text); Digest[k].columns[i].index = c; } } string common; for(int k = 1; k < ArraySize(Digest); k++) { common += GenerateCommonEnums(obj, k); } PUSH(IndexInputs, "\ninput group \"MOEX ISS Index\""); string result = ConvertDigestLevel(obj, 0) + "\n#endif\n"; StringReplace(result, "\n/* ### */\n", "\n#ifdef MOEX_DEMO_INPUTS\n"); string inputs = "\n#ifdef MOEX_DEMO_INPUTS\n" + StringCombine(IndexInputs, '\n') + "\n#endif\n"; return caption + result + common + inputs; }
A função GenerateCommonEnums cria os enumeradores genéricos (MOEX_ENGINES, MOEX_MARKETS, MOEX_BOARDS etc.) — esses serão usados nas demonstrações das próximas seções.
Além disso, a função ConvertDigestLevel gera uma grande quantidade de enumeradores mais específicos e especializados (por exemplo, MOEX_MARKETS_STOCK — para os mercados do engine stock, MOEX_BOARDS_STOCK_INDEX — para os "boards" do mercado index do engine stock, e assim por diante, respeitando toda a hierarquia do índice, com base na estrutura aninhada das entidades). Esses enumeradores serão úteis para abordagens alternativas de geração de código (algumas já tratadas na parte introdutória do artigo), assim como em projetos próprios de integração com a bolsa — especialmente os que se destinam a mercados específicos, como futuros ou opções. Todos esses enumeradores, junto com seus respectivos parâmetros input, estão cercados por diretivas de compilação condicional MOEX_DEMO_INPUTS e vêm desativados por padrão.
Com relação a esses enumeradores alternativos, vale destacar um detalhe importante. O mesmo nome é frequentemente utilizado para identificar diferentes entidades no ISS: por exemplo, futures é usado como nome de engine, de mercado, de grupo de modos de negociação e de tipo de título financeiro. Entretanto, no MQL5, todos os identificadores dos enumeradores ocupam o mesmo espaço global de nomes, o que significa que não pode haver elementos com nomes repetidos em diferentes enumeradores. Para resolver isso, os elementos desses enumeradores especializados, quando necessário para evitar duplicações, recebem um sufixo de sublinhado ('_').
Além disso, em muitos casos os identificadores são formados segundo o padrão "nome_do_pai·nome_do_elemento", onde o caractere utilizado para unir os dois termos é o '·' (dot — um dos poucos permitidos em identificadores, além de letras latinas e números). Isso foi feito para unificar a nomenclatura das entidades — garantindo que os nomes sempre contenham dois componentes: o contexto e o nome do elemento dentro dele. No ISS, esse padrão de nomenclatura não é seguido de forma consistente: em alguns casos o nome do elemento já inclui o contexto do pai (por exemplo, "futures_spread" — um spread de calendário no contexto do engine futures), enquanto em outros casos isso não acontece ("option_on_commodities" — uma opção sobre commodities também no contexto do engine futures, mas sem mencionar futures no nome). O gerador de código, nesses casos, cria um elemento de enumerador com o nome "futures·option_on_commodities". Essa padronização facilita o processamento algorítmico.
De forma análoga ao índice, o diretório de requisições é convertido em um arquivo de cabeçalho chamado moexref.mqh (ou moexref-YYYYMMDDHHMM.mqh, se rodado sob o depurador).
void ConvertReference() { string data = GetData(MoexIssAPIReference); string optionalAPI = GetData(OpenAPIjson); string header = MoexReference2MQL5Converter(data, optionalAPI); if(StringLen(header)) { const string filename = StringFormat("MOEX/moexref%s.mqh", TIMESTAMP); if(WriteTextFile(filename, header)) { if(Logging) PrintFormat("File saved: %s", filename); } } WriteIncFiles(); // пишем отдельные inc-файлы со структурами, согласно каждой секции API section.datatype }
A função MoexReference2MQL5Converter, assim como muitas funções auxiliares, será deixada fora do escopo deste artigo. No interior do gerador de código, foi necessário tratar diversos aspectos particulares da ISS API e aplicar algoritmos variados.
A geração é baseada em uma navegação recursiva por cada caminho da API, percorrendo seus fragmentos a partir da raiz e acumulando propriedades dos elementos (identificadores, tipos, enums, protótipos de funções etc.) em diversos arrays, que serão usados depois para montar diferentes estruturas no novo código gerado.
As características dessa implementação ficam como sugestão para estudo por parte do leitor, incluindo os principais arquivos de cabeçalho necessários para o funcionamento do gerador de código:
- moexlink.mqh — envio e tratamento de requisições web;
- moexdigest.mqh — estrutura raiz do índice ISS em formato de "digest" (array de estruturas já descrito anteriormente);
- moexutils.mqh — funções auxiliares;
- moexinit.mqh — palavras-chave do MQL5 usadas para resolver conflitos com nomes do ISS;
- toyjson2.mqh — parser JSON;
Os arquivos moexindex.mqh e moexref.mqh, gerados com o uso do codificador, estão disponíveis junto ao artigo. Vale destacar que o arquivo moexindex.mqh já está incluído dentro do moexref.mqh, portanto, no seu projeto, basta incluir um único cabeçalho — moexref.mqh — e ele irá importar todas as dependências necessárias, entre elas:
- moexcore.mqh — definição das estruturas base sobre as quais os códigos gerados se apoiam;
- moex2mql5.mqh — conversão dos dados ISS para estruturas MQL5 de uso prático (ver seção de integração aplicada a seguir);
- htmlstrp.mqh — parser simplificado de páginas HTML (ver seção de tratamento de erros mais adiante);
Após a geração do código, é necessário copiar os arquivos moexref.mqh, moexindex.mqh e toda a pasta /inc de MQL5/Files/MOEX para a pasta do seu projeto.
Integração aplicada no MQL5
Até agora, o desenvolvimento do programa foi orientado pela organização e estrutura dos dados definidos na especificação do ISS, bem como pelas particularidades técnicas desse serviço web. Agora chegou o momento de conectar esse “mundo” ao “mundo” do MQL5.
Para a biblioteca baseada em código gerado automaticamente, o resultado de uma requisição bem-sucedida ao ISS é um objeto JSON contendo determinados dados de aplicação. A conversão desses dados para as estruturas nativas do MQL5 é feita pelas funções reunidas no arquivo moex2mql5.mqh. As seguintes tarefas estão implementadas:
- conversão de intervalos em timeframes;
- criação de símbolo personalizado com base na especificação do ISS;
- obtenção do intervalo de datas históricas para um símbolo;
- extração de um array de cotações MqlRates;
- extração de um array de ticks MqlTick;
- extração do book de ofertas como um array de MqlBookInfo;
A conversão de intervalos em timeframes é feita pela função Interval2Timeframe, mas como se trata de uma tarefa bastante simples, o código-fonte e as explicações foram omitidos.
Para criar corretamente um símbolo personalizado, o ideal é fazer duas requisições ao ISS:
- /iss/securities/[security]
- /iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]
A primeira retorna uma descrição detalhada do símbolo (bloco "description") e a lista de modos de negociação disponíveis para ele ("boards"); a segunda fornece os resultados agregados de negociação ("securities") e as informações de mercado em tempo real ("marketdata"). Embora muitos campos se repitam, há também diferenças que se complementam, especialmente entre os diversos mercados. Por isso, faz sentido unir os dois objetos JSON e criar o símbolo personalizado com base nessa informação combinada. Como veremos mais adiante no exemplo, os objetos JSON podem ser fundidos com o método merge da biblioteca ToyJson2. Em seguida, essa informação composta pode ser passada para a função Moex::CreateCustomSymbol do arquivo moex2mql5.mqh — o JSON entra como primeiro parâmetro, chamado spec.
bool CreateCustomSymbol(JsValue *spec, const string symbol, const string market, const string engine, const string currency = NULL) { ColumnsToProperties(spec); // ценовые свойства и лот int d = spec["securities"]["0"]["decimals"].get<int>(true, -1); double tick = spec["securities"]["0"]["minstep"].get<double>(true, MathPow(10, -d)); double stepprice = spec["securities"]["0"]["stepprice"].get<double>(true); int l = spec["securities"]["0"]["lotvolume"].get<int>(true, -1); if(l == -1) l = spec["securities"]["0"]["lotsize"].get<int>(true, -1); if(l == -1) l = 1; // умолчание // ищем подходящий режим расчетов по названиям рынка и движка int calcmode = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_CALC_MODE); int elem[], max = 0, best = -1; const int n = EnumToArray<ENUM_SYMBOL_CALC_MODE>(elem, 0, 100); for(int i = 0; i < n; ++i) { string c = EnumToString((ENUM_SYMBOL_CALC_MODE)elem[i]); StringToLower(c); int mark = (StringFind(c, market) >= 0) + (StringFind(c, engine) >= 0); if(mark > max) { max = mark; best = i; } } if(best != -1) { calcmode = elem[best]; } else { Print("No Calcmode, defaults to current: ", EnumToString((ENUM_SYMBOL_CALC_MODE)calcmode)); } // берем "SHORTNAME" и "SECNAME" в качестве описания string desc = spec["securities"]["0"]["shortname"].s + "," + spec["securities"]["0"]["secname"].s; // выясняем валюту котирования и расчетов, но она м.б. пуста - заполним ниже string profit = spec["securities"]["0"]["currencyid"].get<string>(true); if(profit == "SUR") profit = "RUB"; // диапазон дат листинга datetime start = 0, stop = 0; string basis; string unit, words[]; if(spec["description"].t != JS_NULL) { JsValue *props = MapSectionData(spec["description"], "name", "value", true); // колонки ("FRSTTRADE", "LSTTRADE", "CONTRACTNAME") из секции 'description' запроса iss/securities/XYZ start = StringToTime(props["frsttrade"].s); stop = StringToTime(props["lsttrade"].s); basis = props["contractname"].s; unit = props["unit"].s; // NB: требуется для запроса выбрать английский язык "lang=en" if(StringLen(unit)) // бывают разные формулировки, например "RUB per 10 lots", "USD per 1 MMBtu" { if(StringSplit(unit, ' ', words) > 3) { profit = words[0]; // уточняем валюту котирования } } delete props; } else { // колонка "LASTTRADEDATE" из секции 'securities' запросов /iss/engines/futures/markets/forts.json или .../forts/securities/XYZ stop = StringToTime(spec["securities"]["0"]["lasttradedate"].s); } if(!StringLen(profit)) // если валюту не нашли и не задали явно, то по умолчанию на MOEX рубли { profit = StringLen(currency) ? currency : "RUB"; } // создаем кастом-символ с собранными свойствами if(CustomSymbolCreate(symbol, "MOEXISS\\" + engine + "\\" + market, NULL)) { CustomSymbolSetInteger(symbol, SYMBOL_TRADE_CALC_MODE, calcmode); CustomSymbolSetInteger(symbol, SYMBOL_DIGITS, d); CustomSymbolSetDouble(symbol, SYMBOL_VOLUME_MAX, 1000); // NB: нет информации CustomSymbolSetDouble(symbol, SYMBOL_VOLUME_MIN, 1); CustomSymbolSetDouble(symbol, SYMBOL_VOLUME_STEP, 1); CustomSymbolSetDouble(symbol, SYMBOL_TRADE_TICK_SIZE, tick); CustomSymbolSetDouble(symbol, SYMBOL_TRADE_TICK_VALUE, stepprice ? stepprice : l * tick); CustomSymbolSetDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE, l); CustomSymbolSetString(symbol, SYMBOL_DESCRIPTION, desc); CustomSymbolSetString(symbol, SYMBOL_CURRENCY_PROFIT, profit); CustomSymbolSetString(symbol, SYMBOL_CURRENCY_MARGIN, profit); CustomSymbolSetInteger(symbol, SYMBOL_CHART_MODE, SYMBOL_CHART_MODE_LAST); if(start) CustomSymbolSetInteger(symbol, SYMBOL_START_TIME, start); if(stop) CustomSymbolSetInteger(symbol, SYMBOL_EXPIRATION_TIME, stop); if(StringLen(basis)) CustomSymbolSetString(symbol, SYMBOL_BASIS, basis); SymbolSelect(symbol, true); return true; } return false; }
O símbolo é criado na pasta MOEXISS, com subpastas organizadas pelos nomes do engine e do mercado. Para descobrir corretamente a moeda do ativo, é necessário incluir o parâmetro adicional "lang=en" na primeira das duas requisições discutidas, pois, caso contrário, o campo "unit" virá em formato livre em russo, como "доллар США" em vez de "USD".
As funções auxiliares utilizadas em CreateCustomSymbol não são explicadas no artigo — consulte os códigos-fonte fornecidos.
A profundidade do histórico de candles para um instrumento pode ser obtida por meio da seguinte requisição:
/iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/candleborders
Ela retorna dados no bloco "borders", onde são especificadas as datas inicial e final disponíveis para cada timeframe.
Esse objeto JSON pode ser convertido para um formato compreensível por um programa em MQL com a função Moex::GetCandleborders.
struct HistoryRange { datetime begin; datetime end; ENUM_TIMEFRAMES timeframe; }; bool GetCandleborders(JsValue *data, HistoryRange &history[], const bool reverify = false) { const static string main = "borders"; if(data[main].t == JS_OBJECT) { // проверяем набор требуемых колонок один раз в сессию или по запросу enum Columns { begin, end, interval }; static ColumnVerificator<Columns> cf; if(reverify) cf.reset(); if(!cf.verifyColumns(data, main)) return false; JsValue *section = data[main]["data"]; const int n = section.size(); if(!n) return true; ArrayResize(history, n); for(int i = 0; i < n; i++) { const JsValue *row = section[i]; HistoryRange range = { StringToTime(row[cf[begin]].s), StringToTime(row[cf[end]].s), Interval2Timeframe(row[cf[interval]].get<int>()) }; history[i] = range; } return true; } return false; }
A classe utilizada aqui, ColumnVerificator, verifica a presença das colunas (propriedades) exigidas no JSON e também memoriza os índices correspondentes, permitindo que, posteriormente, os dados sejam lidos pelo número da coluna (cada índice corresponde ao valor de um elemento do enumerador Columns). Essa classe é empregada também em outras funções de conversão de dados.
Nos trechos anteriores, já vimos exemplos de requisições de candles — elas retornam um JSON com a seção "candles". Para convertê-lo em um array MqlRates, utiliza-se a função Moex::GetRates.
bool GetRates(JsValue *data, MqlRates &rates[], const bool reverify = false) { const static string main = "candles"; if(data[main].t == JS_OBJECT) { enum Columns { open, close, high, low, value, volume, begin, end }; static ColumnVerificator<Columns> cf; if(reverify) cf.reset(); if(!cf.verifyColumns(data, main)) return false; JsValue *section = data[main]["data"]; const int n = section.size(); if(!n) return true; // пустой массив - не ошибка конвертера ArrayResize(rates, n); for(int i = 0; i < n; i++) { const JsValue *row = section[i]; MqlRates r = { StringToTime(row[cf[begin]].s), StringToDouble(row[cf[open]].s), StringToDouble(row[cf[high]].s), StringToDouble(row[cf[low]].s), StringToDouble(row[cf[close]].s), StringToInteger(row[cf[volume]].s), 0, // spread StringToInteger(row[cf[value]].s), }; rates[i] = r; } return true; } return false; }
As negociações (ou "trades") são processadas de forma semelhante e podem ser obtidas do ISS com a seguinte requisição:
/iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/trades.json
As negociações representam os ticks na terminologia do MetaTrader 5. A conversão de JSON para ticks é responsabilidade da classe Trades2Ticks, mais especificamente de seu método GetTicks.
enum TradesColumns { tradeno, recno, tradetime, tradedate, boardid, boardname, secid, price, quantity, value, period, tradetime_grp, systime, buysell, decimals, tradingsession, openposition, offmarketdeal }; template<typename E> class Trades2Ticks { ColumnVerificator<E> cv; public: int operator[](int e) { return cv[e]; } bool GetTicks(JsValue *data, MqlTick &ticks[], const bool reverify = false) { const static string main = "trades"; if(data[main].t == JS_OBJECT) { if(reverify) cv.reset(); if(!cv.verifyColumns(data, main)) return false; JsValue *section = data[main]["data"]; const int n = section.size(); if(!n) return true; ArrayResize(ticks, n); for(int i = 0; i < n; i++) { const JsValue *row = section[i]; const datetime time = StringToTime(row[cv[E::systime]].s); const double price = StringToDouble(row[cv[E::price]].s); const uint flags = TICK_FLAG_BID | TICK_FLAG_ASK | TICK_FLAG_LAST | TICK_FLAG_VOLUME | (row[cv[E::buysell]].s == "B" || row[cv[E::buysell]].s == "b" ? TICK_FLAG_BUY : TICK_FLAG_SELL); MqlTick t = { time, price, price, price, StringToInteger(row[cv[E::quantity]].s), time * 1000, flags, StringToDouble(row[cv[E::quantity]].s) }; ticks[i] = t; } return true; } return false; } };
Neste caso, a lista de colunas pode variar dependendo do mercado, então a classe permite parametrizar o ColumnVerificator com um enumerador personalizado. Por padrão, é fornecido um enumerador contendo um conjunto genérico das colunas mais comuns e relevantes. Em particular, as colunas tradeno ou recno contêm o número exclusivo do tick — com isso, é possível fazer requisições ao ISS buscando apenas os ticks mais recentes, como será demonstrado em um exemplo na seção específica do artigo.
É importante observar que, do fluxo de negociações, só conseguimos extrair um único preço — o last. Para preencher os campos de preços ask/bid da estrutura MqlTick, será necessário fazer outra requisição ao ISS — idealmente, seria o "book de ofertas", mas ele está disponível apenas via assinatura, então recorreremos a outra chamada:
/iss/engines/[engine]/markets/[market]/boards/[board]/securities.json?securities=[security]
Essa requisição retorna dados que incluem, entre outras coisas, a seção "marketdata", onde estão localizadas as colunas "offer" e "bid", que nos interessam. Isso também será mostrado no exemplo.
Para converter "books de ofertas" existe a função Moex::GetOrderbook, semelhante às funções mencionadas acima:
bool GetOrderbook(JsValue *data, MqlBookInfo &book[], const bool reverify = false);
É importante ressaltar que as requisições de books retornam snapshots do estado atual, com uma frequência limitada pelas condições da conexão com o serviço web. Para obter o histórico de books, é possível salvar manualmente os snapshots em um arquivo local ou calcular o book com base no histórico de ordens (disponível por assinatura, por meio das requisições da família /iss/archives).
Exemplos de uso
Vamos demonstrar na prática como funciona a biblioteca de códigos gerados.
Para todos os exemplos, é importante permitir nas configurações do terminal o acesso ao domínio iss.moex.com.
Todos os exemplos compartilham um conjunto comum de parâmetros para configurar o funcionamento da classe MoexLink.
input group "Auxiliary" input bool Logging = false; input bool Dumping = true; input bool Test = false; input uint Timeout = 5; // Timeout (seconds) input uint Delay = 100; // Delay (between paging requests, ms) input string Parameters = "iss.meta=off"; // Global Parameters (such as iss.meta=on|off, etc) void OnStart() { iss.link.applySettings(MoexLink::Settings(Logging, Dumping, Test, Timeout, Delay, Parameters)); ... }
Por padrão, os logs detalhados estão desativados (Logging), mas todos os dados recebidos são salvos em dump (Dumping) para fins de depuração. Caso surja essa necessidade, basta ativar Test = true — nesse modo, as respostas previamente armazenadas no dump serão carregadas do disco, em vez de serem obtidas da internet. Entre duas requisições consecutivas, é inserido um atraso (Delay) de 100ms para evitar sobrecarga no servidor, especialmente ao baixar um histórico longo ou um diretório extenso de instrumentos. O campo Parameters é destinado à definição de opções adicionais fixas em todas as requisições — por exemplo, nele é possível desabilitar por padrão a seção "metadata" (útil apenas ao explorar a estrutura de uma requisição específica).
O script moexmarket.mq5 permite visualizar informações resumidas sobre o mercado selecionado, além de executar uma busca contextual por string ou filtrar por títulos financeiros.
input group "MOEX Navigator" input MOEX_ENGINES UseEngine = stock; input MOEX_MARKETS UseMarket = stock·shares; input group "MOEX Filters" input string TextSearch = ""; input MOEX_SECURITYCOLLECTIONS UseCollection = stock_shares_one; input MOEX_FILTER_BY UseFilter = 0; // UseFilter (1 из 2 след.фильтров) input MOEX_SECURITYTYPES UseTypeFilter = 0; input MOEX_SECURITYGROUPS UseGroupFilter = 0; input group "MOEX Sorting" input MOEX_SORT_ORDER ColumnOrder = 0; input string ColumnName = ""; void OnStart() { const string market = EnumToMOEXString(UseMarket); // 1. Лидеры рынка с сортировкой по любой колонке, например, "NUMTRADES" по убыванию (desc) JsValue *leaders = iss.engines.securities(UseMarket, UseEngine)._security_collection(UseCollection). _sort_column(ColumnName)._sort_order(ColumnOrder)._leaders(YES)._get(); Moex::ColumnsToProperties(leaders); if(Dumping) DumpJsonToFile(StringFormat("MOEX/leaders_%s.json", market), leaders); // 2. Обороты JsValue *turnovers = iss.engines.turnovers(UseMarket, UseEngine)._get(); if(Dumping) DumpJsonToFile(StringFormat("MOEX/turnovers_%s.json", market), turnovers); // 3. Описания всех секций и столбцов в ответах по указанному рынку JsValue *config = iss.engines.market(UseMarket, UseEngine)._get(); if(Dumping) DumpJsonToFile(StringFormat("MOEX/config_%s.json", market), config); // Опционально контекстный поиск и фильтрация if(StringLen(TextSearch) || UseFilter) { JsValue *search = iss.securities.securities()._q(TextSearch)._is_trading(YES). _group_by(UseFilter). _group_by_filter(UseFilter == TYPE ? UseTypeFilter : no_securitytypes). _group_by_filter(UseFilter == GROUP ? UseGroupFilter : no_securitygroups)._get(); Moex::ColumnsToProperties(search); if(Dumping) DumpJsonToFile("MOEX/search.json", search); if(search["securities"]["data"].length()) { // берем 1-й тикер для примера, порядок в полученном массиве не определен JsValue *ticker = search["securities"]["0"]; const string caption = StringFormat("%d tickers found", search["securities"]["data"].length()); Print(caption); Alert(StringFormat("1-st found %s '%s' (%s, %s)\nTraded on '%s' board", ticker["type"].s, ticker["secid"].s, ticker["shortname"].s, ticker["name"].s, ticker["primary_boardid"].s)); const int cmd = MessageBox(StringFormat("The first match: '%s', print the list to log (YES) or exit (NO)", ticker["secid"].s), caption, MB_YESNO); if(cmd == IDNO) { return; } // пример получения id доски как enum; // применимо также для запроса iss/securities/<security>, возвращающего секцию "boards" // с колонкам market_id, engine_id, которые будучи целыми, могут приводиться к соответствующим enum-ам MOEX_BOARDS board = StringToMOEXEnum<MOEX_BOARDS>(ticker["primary_boardid"].s); } else { Alert("Match not found"); } for(int i = 0; i < search["securities"]["data"].length(); i++) { Print(search["securities"]["data"][i].stringify(0, 0)); } } }
O script moexsymbol.mq5 possibilita criar um símbolo personalizado com base no ticker escolhido do ISS e atualiza seus candles e ticks em tempo real. Para que o símbolo personalizado seja gerado corretamente, mantenha o timeframe M1 definido por padrão — o próprio MetaTrader 5 será responsável por calcular os candles dos timeframes superiores. Certifique-se de que o ticker pertença ao mercado selecionado (ou redefina o mercado e o engine para que o programa os escolha automaticamente com base no modo de negociação).
input group "MOEX Navigator" input MOEX_ENGINES UseEngine = stock; input MOEX_MARKETS UseMarket = stock·shares; input MOEX_BOARDS UseBoard = shares·TQBR; input group "MOEX Parameters" input string UseTicker = "AFLT"; input datetime From; input datetime To; input MOEX_INTERVAL Timeframe = M1; void OnStart() { // просто для сведения выводим диапазон дат имеющейся истории (рынок и движок автоопределяются по режиму торгов) JsValue *range = iss.engines.candleborders(UseTicker, UseBoard)._get(); Moex::HistoryRange history[]; PASS(Moex::GetCandleborders(range, history)); if(Logging) ArrayPrint(history); bool isCustom = false; if(!SymbolExist(UseTicker, isCustom)) { Print("Creating new custom symbol ", UseTicker); string market, engine; // получаем имена рынка и движка как строки чтобы определить SYMBOL_TRADE_CALC_MODE и путь символа Moex::ResolveMoexNames(UseBoard, UseMarket, UseEngine, market, engine, !UseMarket || !UseEngine); // расширенная информация по одиночному тикеру iss/securities/XYZ JsValue *about = iss.securities.security(UseTicker)._get(); // спецификация тикера /iss/engines/[engine]/markets/[market]/boards/[board]/securities/XYZ JsValue *spec = iss.engines.security(UseTicker, UseBoard, UseMarket, UseEngine)._sort_column("SECID")._get(); if(spec.t == JS_OBJECT) // не ошибка и не null { if(about.t == JS_OBJECT) // объединяем спецификацию и описание { spec.merge(about, true); } if(Dumping) DumpJsonToFile(StringFormat("MOEX/symbol-%s.json", UseTicker), spec); // создаем кастом-символ по пути engine/market/ticker if(Moex::CreateCustomSymbol(spec, UseTicker, market, engine)) { Print("Successfully created"); isCustom = true; } else { Print("Failed with error: ", _LastError); } } } if(isCustom) // обновляем бары и тики у существующего символа { datetime last = (datetime)SeriesInfoInteger(UseTicker, Moex::Interval2Timeframe(Timeframe), SERIES_LASTBAR_DATE); PrintFormat("Updating custom symbol %s from %s", UseTicker, (string)last); int timing = (int)(Timeout * 1000); if(!last) { JsValue *data = iss.engines.candles(UseTicker, UseBoard, UseMarket, UseEngine). _interval(Timeframe)._from(fmin(From, To))._till(fmax(From, To))._start(MOEX_ALL)._get(); MqlRates rates[]; PASS(Moex::GetRates(data, rates)); if(Logging) ArrayPrint(rates); PRTF(CustomRatesReplace(UseTicker, NULL, LONG_MAX, rates)); } else { const static int DAYLONG = 60 * 60 * 24; long limit = 0; datetime prevday = 0, prevlast = 0, lasttick = 0; long next = 0; int no = 0; // номер последнего известного нам тика int tickCount = 0, rateCount = 0; while(!IsStopped()) { ResetLastError(); JsValue *data = iss.engines.candles(UseTicker, UseBoard, UseMarket, UseEngine). _interval(Timeframe)._from(last)._till(TimeCurrent() + DAYLONG)._start(!limit ? MOEX_ALL : limit)._get(); MqlRates rates[]; PASS(Moex::GetRates(data, rates)); const int n = ArraySize(rates); if(n) { PASS(CustomRatesUpdate(UseTicker, rates)); last = rates[n - 1].time; long delta = prevlast ? (last / 60 * 60 - prevlast / 60 * 60) / 60 : 0; // следующий M1 бар или несколько if(prevlast != last) rateCount += n; prevlast = last; last = last / DAYLONG * DAYLONG; if(last != prevday) limit = 0; // сбрасываем отступ в барах M1 внутри дня при смене дня else limit = fmax(limit + delta, n - 1); prevday = last; // запоминаем последний известный день } if(no != -1) { Moex::Trades2TicksDefault t2t; JsValue *trades; MqlTick ticks[]; if(!next) { trades = iss.engines.trades(UseTicker, UseBoard, UseMarket, UseEngine)._reversed(YES)._limit(1)._get("iss.only=trades"); PASS(t2t.GetTicks(trades, ticks)); } else { trades = iss.engines.trades(UseTicker, UseBoard, UseMarket, UseEngine)._tradeno(next)._next_trade(YES)._get("iss.only=trades"); PASS(t2t.GetTicks(trades, ticks)); } if(trades[0]["data"].size()) { no = t2t[Moex::TradesColumns::recno] > -1 ? t2t[Moex::TradesColumns::recno] : t2t[Moex::TradesColumns::tradeno]; PASS(no != -1); next = trades[0]["data"][trades[0]["data"].size() - 1][no].get<long>(); } const int m = ArraySize(ticks); if(m) { JsValue *online = iss.engines.securities(UseBoard, UseMarket, UseEngine)._securities(UseTicker)._get("iss.only=marketdata"); Moex::ColumnsToProperties(online); const double bid = online["marketdata"]["0"]["bid"].get<double>(); const double ask = online["marketdata"]["0"]["offer"].get<double>(); if(ask != 0.0 && bid != 0.0) { for(int i = 0; i < m; i++) { if(ask > ticks[i].last) ticks[i].ask = ask; if(bid < ticks[i].last) ticks[i].bid = bid; } } PASS(CustomTicksAdd(UseTicker, ticks)); tickCount += m; lasttick = ticks[m - 1].time; } } Comment(StringFormat("Bars:%d; Ticks:%d; Last time: %s", rateCount, tickCount, (string)(lasttick ? lasttick : prevlast))); Sleep(timing); Moex::Base::_fetch(NULL); // время от времени очищаем кеш объектов (т.к. они больше не нужны) } } Comment(""); } }
O resultado da execução para AFLT está mostrado na próxima captura de tela.
Transmissão de ticks de instrumento personalizado a partir da bolsa
E aqui estão as especificações dos futuros importados.
+
Especificações de instrumentos personalizados com base nos dados da bolsa
Tratamento de erros
Durante a comunicação com o serviço web, podem surgir diversos tipos de erro que impedem o recebimento dos dados esperados, embora se manifestem de maneiras diferentes. Parte dos erros (de baixo nível ou de transporte) são detectados na chamada do WebRequest e acionam o flag _LastError — nesses casos, o resultado da requisição é vazio e o código que a chamou deve tratar a situação de algum modo — por exemplo, emitindo um aviso ao usuário (o código de erro é registrado pela classe MoexLink no log, caso o modo de logging esteja ativado). Já os erros de nível superior (aplicacional) ocorrem quando há resposta à requisição, mas o cabeçalho HTTP recebido contém um status diferente de 200 (sucesso). Pode ser, por exemplo, o código 404 (página inexistente) ou da série 500 (problemas no servidor). Nesses casos, o serviço web retorna uma página HTML, e não dados no formato solicitado (no nosso caso — sempre JSON).
Por conta disso, a biblioteca implementa um algoritmo simplificado para extração de texto de páginas HTML (ver htmlstrp.mqh), que é acionado caso ocorra falha ao tentar fazer o parsing da resposta em JSON. Esse recurso fornece informações de diagnóstico úteis para o usuário.
Infelizmente, erros em nível aplicacional às vezes ocorrem onde não deveriam, e por vezes vêm acompanhados de um status HTTP de sucesso (200). Abaixo mostramos alguns exemplos ilustrativos dessas situações.
Exemplos de mensagens de erro em requisições: cima — acesso negado ao orderbook (código HTTP: 200, ou seja, formalmente sucesso),
baixo — requisição formalmente válida, mas não funcional via boardgroups (código HTTP: 404)
Ao desenvolver seus próprios programas, esteja preparado para o fato de que o serviço web ISS pode não funcionar exatamente conforme descrito.
Manutenção
Vale lembrar que o funcionamento da biblioteca depende do diretório da API e do índice do ISS. O primeiro raramente muda — geralmente apenas em atualizações de versão da API — enquanto o segundo muda com frequência, podendo ser atualizado diariamente. Felizmente, as alterações no índice normalmente consistem apenas na mudança de estado de diferentes modos entre ligado/desligado, representada por alterações nos dados de 0 para 1 e vice-versa. Os conjuntos de objetos do índice — seus nomes, identificadores e hierarquia — permanecem estáveis, o que significa que o código-fonte gerado uma vez deve continuar funcionando corretamente.
No entanto, por precaução, recomenda-se baixar o índice atualizado a cada dia e compará-lo com o anterior para detectar eventuais mudanças relevantes.
Lembre-se de que o índice completo do ISS (com todas as entidades de nível superior, exceto os valores mobiliários) pode ser obtido via a requisição /iss/index, que é implementada no código gerado da seguinte forma:
JsValue *index = iss.index.index()._get();
Para facilitar a comparação contextual entre duas versões do índice, foi incluído com este artigo o script jsoncmp.mq5, que analisa dois arquivos JSON convertendo-os em objetos JSON e aplicando sobre eles o método match da ToyJson2. Esse script considera irrelevantes alterações nos campos inteiros com valor 1 -> 0 ou 0 -> 1. Qualquer outra diferença gera um aviso. Espera-se que o usuário avalie pessoalmente as mudanças e, se necessário, regenere a biblioteca.
Conclusão
Neste artigo, exploramos a estrutura interna e as particularidades técnicas do serviço web da bolsa, e implementamos ferramentas para geração automática de código, criando um "wrapper" universal para todo o API público.
Com base nas estruturas e exemplos apresentados, é possível implementar uma ampla gama de ferramentas analíticas que combinam indicadores de nível profissional (provenientes da bolsa) com funcionalidades técnicas de processamento e visualização de dados do MetaTrader 5.
O conjunto de ferramentas proposto pode, possivelmente, ser adaptado para integração com APIs de outras bolsas, embora seja provável que exija adaptações ou até mesmo uma reformulação significativa.
Descrição dos arquivos utilizados no artigo
Catálogo/Arquivo | Descrição |
---|---|
MQL5/Scripts/MOEXISS | |
htmlstrp.mqh | Ferramenta para extração de texto de páginas HTML |
inc | Exemplo de pasta com estruturas geradas automaticamente, incluídas no moexref.mqh |
ISS Queries Reference.html | Página completa da web com a especificação do ISS |
jsoncmp.mq5 | Script para comparação de dois arquivos JSON |
jstraverse.mqh | Classe auxiliar para percorrer objetos JSON |
moex2mql5.mqh | Funções para integração prática de dados do ISS ao MQL5 |
moexcore.mqh | Estrutura base para o wrapper MQL5 do web service ISS |
moexdigest.mqh | Tabela relacional organizacional das entidades ISS para uso em moexindexer.mq5 |
moexindex.mqh | Exemplo de código gerado automaticamente, representando o índice ISS no MQL5 |
moexindexer.mq5 | Script para autogerar o wrapper MQL5 do web service ISS |
moexinit.mqh | Código auxiliar de inicialização para o moexindexer.mq5 |
moexlink.mqh | Envio e tratamento de requisições HTTP |
moexmarket.mq5 | Exemplo de obtenção de informações de mercado a partir do ISS, com base no wrapper MQL5 |
moexopenapiedit.mq5 | Script para aprimorar a especificação do ISS no formato OpenAPI |
moexref.mqh | Exemplo de código gerado automaticamente, representando a especificação ISS no MQL5 |
moexsessions.txt | Descrição das sessões, complementar ao índice ISS em moexindexer.mq5 |
moexsymbol.mq5 | Exemplo de criação e atualização de símbolo customizado com requisição de especificações, candles e ticks do ISS |
moexutils.mqh | Funções auxiliares |
reserved.txt | Lista de palavras-chave do MQL5 para moexindexer.mq5 |
StringUtils.mqh | Funções de manipulação de strings |
toyjson2.mqh | Implementação do parser JSON |
MQL5/Include/MQL5Book | |
AutoPtr.mqh | Classe de ponteiro automático |
Defines.mqh | Macros de uso comum |
EnumToArray.mqh | Decodificação de enumeração para array |
MapArray.mqh | Array associativo |
MqlError.mqh | Mensagens de erro do MQL5 |
PRTF.mqh | Saída de expressões de depuração no log |
MQL5/Files/MOEX | |
index.json | Arquivo de índice do serviço ISS (para o gerador de código) |
reference.json | Lista de endpoints do serviço ISS (para o gerador de código) |
moex-iss-api-mod.json | Especificação não oficial do ISS no formato OpenAPI (aperfeiçoada, para o gerador de código) |
moex-iss-api.json | Especificação não oficial do ISS no formato OpenAPI (original, para referência) |
index-new.json | Exemplo de arquivo de índice com alterações (para teste de comparação de arquivos JSON) |
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/16964
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.





- 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
Stanislav, obrigado pelo excelente trabalho que você fez. Esse caso raro, quando o material do artigo é um livro didático em nature.... Tenho uma pergunta como esta. No arquivo moexindex.mqh, na definição da enumeração MOEX_SECURITYTYTYPES, vejo isso:
Por exemplo, uma constante nomeada é definida como currency-gold_metal. Provavelmente, a entrada correta poderia ser currency_gold_metal.
E assim, o novo compilador (build 5200) jura que os nomes de alguns identificadores coincidem:
Por exemplo, uma constante nomeada é definida como currency-gold_metal. Provavelmente, a entrada correta poderia ser currency_gold_metal.
E assim, o novo compilador (build 5200) jura que os nomes de alguns identificadores coincidem:
O caractere "ponto" (- no meio, código Unicode 0x00B7), assim como muitos outros, foi permitido para uso em nomes de variáveis MQL5 e ainda é permitido em C++. Em particular, a citação cpprefernce:
O primeiro caractere de um identificador válido deve ser um dos seguintes:
Qualquer outro caractere de um identificador válido deve ser um dos seguintes:
Por sua vez, o grupo de caracteres XID_Continue pode ser encontrado entre outros grupos em unicode.org.
Não sei por que o compilador anterior era compatível com o C++ e o novo não.
Escreverei sobre isso no tópico sobre a versão mais recente, mas não posso mudar nada de minha parte, pois essa solução técnica foi explicada no artigo:
Em outras palavras, diferentes entidades na hierarquia MOEX podem usar a mesma palavra e, portanto, é necessário algum tipo de caractere delimitador no identificador entre o contêiner e o elemento aninhado. Não é possível usar apenas o nome abreviado do elemento, pois haverá duplicatas imediatamente. Não é possível usar sublinhados porque eles já são usados em nomes de elementos MOEX. Não é possível usar a codificação posicional porque o número de palavras no nome do elemento é desconhecido.
Por exemplo, uma constante nomeada é definida como currency-gold_metal. Provavelmente, a entrada correta poderia ser currency_gold_metal.
E assim, o novo compilador (build 5200) jura que os nomes de alguns identificadores coincidem:
Após uma análise mais detalhada do problema, estou inclinado a pensar que o erro não está relacionado ao símbolo de ponto, mas ao fato de que os nomes dos itens de enumeração agora estão consolidados entre todas as enumerações.
Não tenho ideia de como corrigir isso no nível do aplicativo. Se você adicionar o nome da enumeração ao identificador da variável, não terá comprimento suficiente permitido para a variável na maioria dos lugares.
Acho que o mais fácil por enquanto é editar as duplicatas manualmente, por exemplo, para adicionar números.
Sim, ocaractere de ponto (- no meio, código Unicode 0x00B7) funciona.
Há outro problema. Tentei alterar as enumerações. Em particular:
O compilador funcionou, pelo menos, sem erros. Mas. Ao compilar o URL, o script moexmarket produziu esse URL:
onde tem isso no final da linha:
security_collection= sec_coll_stock_shares_one
Aparentemente, precisamos de alguma forma modificar a função nativa EnumToString() para obter stock_shares_one em vez de sec_coll_stock_shares_one.
onde, no final da linha, há isto:
Aparentemente, precisamos de alguma forma modificar a função nativa EnumToString() para obter stock_shares_one em vez de sec_coll_stock_shares_one.
Bem, se uma resolução de conflito única for suficiente, tente isso:
Embora especificamente com essa enumeração, não esteja claro para mim por que os elementos entram em conflito com algo - em particular, stock_shares_one está apenas em MOEX_SECURITYCOLLECTIONS (uma lista geral de coleções) e em MOEX_SECURITYCOLLECTIONS_STOCK_SHARES (uma coleção específica para uma determinada seção, uma de muitas), que não está conectada em meus exemplos, pois.porque todas essas enumerações "pequenas" são cobertas pela diretiva MOEX_DEMO_INPUTS e desativadas por padrão - elas são reservadas para uma abordagem diferente de criação de programas a partir do intercâmbio de APIs, em que os links são definidos de forma mais rigorosa no momento da compilação (ou seja, grosso modo, quando o programa é destinado a uma seção específica).