
Биржевые данные без посредников: подключаем MetaTrader 5 к MOEX через ISS API
У каждого человека есть свои "хотелки" — как в глобальном смысле, так и в узкопрофильном, например, в сфере трейдинга, если вы им увлекаетесь или занимаетесь профессионально. Проблема в том, что на практике получить желаемое — например, набор торговых инструментов, уровень сервиса брокера, удобный клиентский терминал — не всегда возможно. В частности, для жителей России трудно найти брокера, который бы одновременно предоставлял и доступ к Московской Бирже, и терминал MetaTrader 5. Как минимум — выбор небогат и заставляет искать альтернативные варианты. Допустим, что променять MetaTrader 5 на что-то другое мы не готовы, что тогда остается?
Предлагаю воспользоваться открытыми веб-сервисами MOEX, которые станет легко... или, по крайней мере, не так уж и сложно... интегрировать с терминалом после прочтения данной статьи. В ней речь пойдет о самом простом и доступном бесплатно сервисе ISS — Информационно-статистическом сервере Московской Биржи. Говоря научным языком — это связка интернет-протокола HTTP и технологии REST-сервиса, а по-простому — то, что мы можем запросить в виде удобочитаемых веб-страниц из обычного браузера или через специально написанную программу для скачивания и анализа данных других форматов, более удобных для программ, например, xml, csv, json.
Биржа предоставляет и более продвинутые, но потому платные, сервисы, построенные по аналогичным техническим принципам. Поэтому потренировавшись на ISS, желающие смогут усовершенствовать текущее решение для выполнения расширенного круга задач.
Если описывать возможности ISS в общих чертах, то он позволяет получать списки и спецификации инструментов, котировки, тики/сделки (за текущий день, история — по подписке), статистические данные (например, по оборотам и открытому интересу), текущие стаканы заявок (по подписке) и многое другое. Бесплатные данные передаются с задержкой 15 минут, но для анализа и выявления внутридневных торговых сигналов с частотой, ниже чем в стратегиях HFT, этого вполне достаточно.
Перечень так называемых "конечных точек" (endpoints), которые олицетворяют конкретные прикладные запросы (команды) сервера, приведен ниже. Хотя на этом начальном уровне (из всех API биржи) нет команд для выставления приказов и полноценной торговли, даже получаемая индикативная информация позволяет существенно пополнить инструменты анализа рынка, увидеть его полную картину и улучшить принимаемые торговые решения.
Рядовые трейдеры сегодня совершают сделки на бирже, как правило, через терминал своего брокера (веб-приложение в браузере или отдельное десктопное приложение), где нет столь продвинутых возможностей, как в MetaTrader 5 — будь то технический анализ (в том числе, с кастом-индикаторами и богатым набором инструментов из кодебазы) или тестирование стратегий на истории. Профессиональные трейдеры, скорее всего, предпочтут интеграцию с использованием протоколов FIX, TWIME, Plaza II и т.д., также поддерживаемых MOEX, но и им зачастую проще проверять свои идеи на MQL5, чем полностью повторять уже готовые и встроенные в MetaTrader 5 технические средства и алгоритмы в собственных программах. Именно поэтому возникает интерес к интеграции ISS API биржи в терминал.
Задача данной статьи — подготовить библиотеку, отображающую ISS API в среду MQL5 максимально естественным образом — с реализацией всех необходимых технических особенностей (скрытых внутри) и вместе с тем, с логическими и синтаксическими подсказками, видными прикладному программисту и, по необходимости, конечному пользователю. Хотелось бы, разумеется, добиться максимально полного охвата ISS API, но на пути к этой цели есть кое-какие препятствия, о чем подробно рассказано ниже.
Исходные данные
Биржа предоставляет массу документации по ISS: руководство разработчика, примеры выполнения запросов, список конечных точек и так называемый индекс — справочник информационных сущностей и их оргструктуру — фактически это дерево, которое мы постепенно рассмотрим в деталях, и из него, как листья, далее "вырастают" спецификации конкретных инструментов, отчеты, архивы и вообще правила работы с API (в каждой ветви — свои "законы").
Два основных среза представления ISS API — это список конечных точек и индекс. Если первый описывает доступные действия, то второй — данные, над которыми эти действия выполняются.
Конечные точки логически разделены на несколько функциональных групп, основные из них:
- получение онлайн-данных о торговле;
- доступ к истории;
- статистические показатели;
- аналитика;
- архивы документов;
Индекс, в свою очередь, включает такие разделы, как:
- движки ("engines"), самые крупные подразделения верхнего уровня;
- рынки ("markets"), более мелкие элементы в составе конкретного движка;
- режимы торгов ("boards") и группы режимов торгов ("boardgroups"), специфичные для каждого рынка;
- типы, группы и коллекции ценных бумаг, торгуемых в разных режимах;
Все это мы рассмотрим более подробно позднее. Пока важно отметить, что элементы индекса выступают параметрами при вызове функций конкретных конечных точек.
К сожалению, список конечных точек представлен на сайте биржи, как разветвленная веб-страница. Его построение и внутреннее форматирование глав — страниц с описанием параметров конкретных запросов, которые открываются по клику на соответствующей ссылке в списке, наводит на мысль, что он сгенерирован из спецификации в формате OpenAPI. Если бы у нас был доступ к полной и актуальной спецификации ISS в формате OpenAPI, это упростило бы кодогенерацию, потому что формат OpenAPI изначально создавался для машиночитаемых описаний API, по которым удобно генерировать заготовки кода как для программ-клиентов, так и программ-серверов, реализующих соответствующий API. На момент написания статьи, официального OpenAPI биржи публично не выложено, поэтому воспользуемся доступными веб-ресурсами.
В принципе, html-страница со списком представляет собой структурированный текст, из которого довольно легко вычленить непосредственно рабочие точки при некотором знании JavaScript. Например, достаточно в любом Chromium-совместимом браузере открыть страницу со списком, нажать Ctrl+Shift+I для попадания в режим разработчика, перейти на вкладку Console или прямо на первой вкладке Elements нажать Esc (появится консоль поменьше), и в строке консоли можно вводить JavaScript.
[...document.querySelectorAll('dt a')].map(e => e.innerText)
Это выражение соберет все конечные точки в массив, который мы сохраним, например, в текстовый файл reference.json (приложен к статье) и будем использовать на входе кодогенератора.
Получение списка конечных точек из веб-страницы справочника ISS
Каждая конечная точка — это путь на сервере https://iss.moex.com/, то есть, состыковав адрес сервера и адрес конечной точки, мы получим URL, который можно ввести прямо в браузере или запросить с помощью WebRequest из MQL5 — и в результате мы получим те или иные данные. Например, для получения вышеупомянутого индекса корневых сущностей сервера, достаточно ввести URL:
https://iss.moex.com/iss/index
Как мы увидим в дальнейшем, во многих адресах присутствуют слова в скобках — это параметры, вместо которых в адрес нужно подставлять идентификаторы конкретных объектов с интересующей нас информацией. Например, в следующем пути требуется указать название конкретного движка [engine] и конкретного рынка [market] в нем, чтобы получить список имеющихся в них режимов торгов.
https://iss.moex.com/iss/engines/[engine]/markets/[market]/boards
Сервер умеет отдавать данные в одном из нескольких форматов:
- HTML;
- XML;
- CSV;
- JSON;
Для указания формата нужно добавить соответствующее расширение к пути запроса, например,
https://iss.moex.com/iss/engines/[engine]/markets/[market]/boards.json
Без указания расширения, сервер по умолчанию отдает HTML-страницу, удобную для чтения человеком. Однако для алгоритмического анализа данных лучше выбрать более структурированный формат, поэтому в дальнейшем мы будем использовать JSON — он более экономный, чем XML, и более гибкий, чем CSV, а кроме того — отлично подходит для ООП.
Следует отметить, что опубликованный список неполный, содержит неточности и опечатки, по сравнение с фактическим положением вещей. Это было частично исправлено в прилагаемом файле. Также, для вашего удобства, к статье прилагается веб-страница ISS Queries Reference.html, где все конечные точки описаны в "плоском" виде, то есть без необходимости переходить по ссылкам для чтения их подробного описания и параметров.
Вот список справочника API ISS, по алфавиту, с сокращениями и выделением основных групп:
[
"/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",
]
По названиям легко догадаться, что некоторые запросы ориентированы на рынок акций, некоторые — на срочный рынок, а другие — для облигаций или валют. Смысл каждого запроса можно понять по последнему фрагменту пути. Например, строки, оканчивающиеся на /securities вернут список инструментов для соответствующего контекста (рынка, режима торгов, сессии и т.д.). Строки с параметром [security] на конце выполняют запрос информации по конкретному символу (спецификация, текущие данные о ходе торгов и т.д.). Такие же говорящие названия у путей с "суффиксами": /candles (свечи), /orderbook (стакан), /trades (сделки, т.е. тики), /turnovers (обороты), /dates (диапазоны дат) и т.д.
Особняком в этом списке стоит "/iss/index" — запрос для получения общего индекса сервиса. С этого запроса, как правило, начинается работа любой новой программы или сервиса. В дальнейшем, на протяжении дня, следует использовать сохраненную у себя версию. В принципе, наиболее вероятные изменения в индексе связаны с переключением опций (0/1, выкл/вкл), влияющих на те или иные нюансы в работе, а наборы движков, рынков, режимов остаются стабильными продолжительное время. Эта особенность позволяет отобразить организационную структуру индекса в иерархии исходных кодов софта для биржи.
Приведем небольшой фрагмент в формате JSON, чтобы вы получили представление о структуре индекса:
{
"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],
...
]
}}
Хотя формат JSON не предназначен для форматирования таблиц в привычном виде, разработчики MOEX "упаковали" таблицы в JSON как массивы массивов ("data") и снабдили их отдельным массивом-шапкой ("columns") с названиями колонок. Данный подход используется в ответах на все запросы к ISS, для формата JSON.
Важно отметить, что таблицы всех сущностей связаны между собой отношениями "владелец<->подчиненный", для установления которых используются уникальные идентификаторы (id). Например, элементы таблицы "securitycollections" имеют помимо собственного идентификатора ("id"), идентификатор родительского элемента "security_group_id", который нужно искать по значению в колонке "id" уже связанной таблицы "securitygroups".
Для наглядности вся эта структура приведена на следующей UML-схеме.
Композиционная UML-диаграмма сущностей ISS
В исходном коде нашей программы потребуется неким образом описать эти же отношения, чтобы проверять правильность вызовов. Соответствующие инструкции собраны в файле 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} };
Каждая строка в таблице Digest описывает отдельный тип сущности в ISS. Помимо типа (поле entity), имеется ссылка на родительский объект (поле relative с номером строки более "высокоуровневой" сущности). Каждый уровень имеет 4 описательных свойства MoexColumn, считываемых из колонок в таблицах скачанного индекса. Нам важны идентификатор, краткое название, расширенное описание и имя колонки с идентификатором родительского элемента.
Например, в строке с ключевым словом "boards" в начале мы видим, что идентификатор, название и описание "доски" берутся из таких колонок: "id", "boardid", "board_title" (разумеется, их надо искать в одноименной индексной таблице "boards"). Идентификатор родительского элемента, к которому относится конкретная "доска", содержится в колонке "market_id". А номер сноски relative (1) подсказывает, что информацию по этому рынку (и по всем рынкам) следует искать в строке дайджеста под номером 1. И там действительно читаем ключевое слово "markets" и аналогичные сведения о колонках со свойствами рынков. В самой первой строке располагается описание "engines", для которых уже нет вышестоящего уровня иерархии, и потому там поле relative равно -1.
Структура MoexColumn потребовалась для того, чтобы по известному имени колонки (поле text) конкретной таблицы однократно узнать порядковый номер этой колонки (поле index) и затем быстро считывать свойства при анализе индекса.
За счет просмотра индекса по правилам дайджеста мы сможем генерировать любые конструкции MQL5, основанные на элементах MOEX ISS.
Но какие именно конструкции?
Концепция
Обычный подход для решения программистской задачи на основе какого-либо API подразумевает изучение необходимой основы (базы API) и отдельных специфических частей или функций, связанных с непосредственной задачей. При таком подходе, если потребуется выполнить другую задачу из состава того же API, нужно будет изучить другие части и написать для них код.
В случае трейдинга, сложно представить задачу, которая бы не включала обращения сразу к нескольким разделам API, причем у каждого трейдера будет свой набор частей (требований). Несмотря на то, что ISS API — это API начального уровня (а может быть и как раз в силу своего обобщенного характера), оно довольно объемное. И потому встает вопрос — какие части API следует реализовать на MQL5 сразу, а какие оставить "на потом"?
Чтобы не искать компромиссов, было принято решение поддержать API целиком (или насколько возможно полно, т.к. существуют сложности, которых коснемся дальше), а проблему с шириной охвата различного функционала решить технологично и кардинально — с помощью кодогенерации.
Кодогенерация — особый подход в программировании, когда мы сначала реализуем другую программу, чтобы потом делегировать ей написание исходной программы. На первый взгляд это сложнее. Но если начальная задача, как обычно бывает, начнет разрастаться, то мы можем существенно сэкономить на рутинном труде/времени и исключить человеческие ошибки, вроде невнимательности или copy&paste.
Кодогенерация чем-то напоминает то, как сегодня поручают "сваять" исходный код сервисам вроде ChatGPT и аналогичным "болтунам". Отличие в том, что наша кодогенерация должна быть строгой:
- соответствовать спецификации ISS API;
- демонстрировать чистый и корректный код на MQL5, компилирующийся без ошибок;
- иметь стройную архитектуру — модульную и расширяемую;
- обеспечивать удобство пользования (для ...);
Озвученные требования на текущий момент можно удовлетворить, только если создавать кодогенератор самим, то есть вручную и без помощи ИИ. В принципе, существенно облегчить задачу или даже полностью её автоматизировать позволила бы какая-нибудь программа генерации кодов MQL5 по OpenAPI-описанию сервиса. Однако, поскольку ни того, ни другого еще не существует, нам и придется реализовать что-то подобное.
Многоточие в последнем пункте стоит потому, что пользователями продукта могут потенциально выступать и MQL5-программисты, и простые трейдеры. Для каждой из этих двух категорий критерии удобства различаются. Поэтому принципы работы и архитектуру библиотеки следует выбрать заранее. Гипотетически, можно поддержать в единой библиотеке несколько подходов, но лучше это делать постепенно, а не в первой версии.
Основная разница в проектировании библиотеки под программистов или под пользователей заключается в следующем. Для программистов лучше, чтобы всё API было построено со строгой типизацией и прописыванием правил и параметров из спецификации непосредственно в исходном коде. Это позволит вбивать исходный код нового прикладного продукта, на лету получая интеллектуальные подсказки от редактора, и гарантирует правильность программы еще на стадии компиляции, потому что MQL5-синтаксис библиотеки основан на спецификации биржи.
Однако, такой подход затрудняет написание программы в универсальном стиле — с возможностью адаптации под разные рынки и режимы уже в готовом виде. А для пользователей как раз важна гибкость и возможность настройки в ходе эксплуатации. Для этого желательно применять внутри свободную типизацию, то есть, грубо говоря, описывать универсальные объекты "рынок" и "режим торгов", а правильность сочетания конкретного выбранного рынка и режима делать только во время выполнения программы.
Продемонстрируем схематично подход со строгой типизацией. Например, зная структуру вложения ранее описанных элементов ISS, мы могли бы спроектировать такие классы:
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); } }; }; }; };
Шаблоны предполагается типизировать перечислениями, сгенерированными на основании индекса ISS. Тогда в коде можно описывать конкретные классы:
// описываем объект для работы с нужным движком, рынком и режимом торгов Moex::Engine<MOEX_ENGINES>::Market<MOEX_MARKETS_STOCK>::Board<MOEX_BOARDS_STOCK_SHARES> b(stock, shares, TQBR); // выполняем запрос b.process();
Здесь идентификаторы stock, shares, TQBR относятся к элементам перечислений (enum) — соответственно, MOEX_ENGINES, MOEX_MARKETS_STOCK, MOEX_BOARDS_STOCK_SHARES — и все они в точности отражают взаимосвязи из индекса, перенесенные (гипотетическим) кодогенератором в MQL5.
Напомним, что перечисления движков, рынков, "досок" и прочих сущностей могут сочетаться лишь в строгом соответствии с иерархией, описанной в индексе. В частности, элементы MOEX_MARKETS_STOCK могут использоваться только для движка stock, как легко понять из названия. Но для компилятора эти слова — пустой звук, и потому необходимо синтаксически как-то декларировать налагаемые ограничения. В приведенном выше псевдокоде эта роль отведена вызовам шаблонных методов Bind, которые кодогенератор создаст только для пар типов в разрешенных сочетаниях.
Если программист попытается описать объект несочетаемых типов (например, в указанном примере введет рынок forts вместо shares, причем рынок forts на самом деле относится к движку futures, а не stock), компиляция завершится ошибкой.
Из этого умозрительного эксперимента можно сделать вывод, что подход с шаблонами хоть и обеспечивает строгую типизацию, но не очень удобен из-за того, что проверка откладывается до стадии компиляции, когда происходит инстанцирование объектов. Кроме того, нотация с шаблонами получается слишком громоздкой.
Более практичным для программистов был бы вариант кодогенератора, который создает уже полностью готовые классы или структуры на основе индекса ISS. Количество автогенерируемого кода при этом в разы увеличится, но зато ошибки прикладной разработки полностью исключены, и прямо во время ввода выражений в редакторе программист увидит выпадающие списки с допустимыми именно в текущем контексте вариантами продолжения — методами или свойствами.
Подсказки MetaEditor в экспериментальном наборе классов ISS со строгой типизацией
К сожалению, полученная программа окажется "заточенной" на конкретную задачу, и настройка поведения в зависимости от входных переменных ограничена (или потребует рутинного кодирования, чтобы предусмотреть множество предопределенных вариантов использования — ведь уместить все функции API ISS в одну программу сродни разработке собственного терминала).
Исходя из вышеизложенного, было принято решение сделать кодогенератор на основе гибкого подхода — с максимально обобщенным набором типов: все рынки собраны в едином перечислении (MOEX_MARKETS), все режимы торгов — в другом (MOEX_BOARDS), и так далее — для каждого корневого раздела в индексе ISS. Правильный подбор элементов пользователем оставим на этап выполнения программы, но поручим кодогенератору создать заготовки функций для всех вариантов проверок.
Но для начала, определимся с тем, что должно получиться на выходе кодогенератора.
Проектирование
В качестве строительных блоков библиотеки, которая получится на выходе кодогенератора, мы можем использовать: структуры, классы, объединения и даже просто функции. Я попробовал множество вариантов и пока остановился на относительно очевидной иерархии структур, повторяющей организацию справочника API. Позднее можно продолжить эксперименты с другими подходами.
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;
Все структуры унаследованы от некоей базовой структуры Base, где мы соберем общие функции. Каждая вложенная структура называется по отличительному начальному фрагменту пути и должна включать методы для обращения к конечным точкам соответствующей группы справочника (он был приведен выше).
Напомню, что путь каждой конечной точки может содержать параметры, описанные в скобках — их потребуется передавать в метод. Сам метод имеет смысл называть по последней части пути, поскольку именно там заключен смысл запроса. Например, в группе/структуре Engines имеется конечная точка:
/iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/candles
Как нетрудно догадаться, она предназначена для получения свечей ("candles") заданного символа (параметр "security"), торгуемого в режиме "board", рынка "market", движка "engine". Значит, опишем внутри структуры метод candles с соответствующим набором параметров:
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; }
Почему мы перевернули порядок следования параметров? Во-первых, для каждого запроса наиболее важным является последний (если читать по спецификации) аргумент. Так, в данном пути последним аргументом передаётся название символа, например, "GOLD", "AFLT" и т.д. Именно конкретная ценная бумага в первую очередь интересует конечного пользователя, а не то, каким образом до неё добраться на бирже. В данной нотации имя метода (выполняемое действие/запрос) и название символа, для которого он выполняется, оказываются рядом в строке кода, и это наглядно.
Во-вторых, мы ранее разобрались с иерархией сущностей биржи, и знаем, что "доски" относятся к конкретным рынкам, а рынки — к конкретному движку. Таким образом, выбрав режим торговли, мы можем опустить остальные аргументы и предоставить программе самой определить выбор правильного рынка и движка. А как мы знаем из правил MQL5, в описании функции можно опускать только конечные элементы из списка параметров.
Что такое Moex::Entity? Это структура-заглушка (в пространстве имен Moex), которая по умолчанию будет прописываться в качестве результата каждой функции, если мы не знаем о ней дополнительных сведений (откуда они возьмутся — расскажем чуть позже).
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; };
Здесь показана реальная реализация Entity и маленький фрагмент Base. Entity демонстрирует простейшей вариант обработки запроса в методе _get: закачивает данные в формате JSON по заданному адресу в поле _url с помощью унаследованного метода _fetch. Поле _url определено в родительской структуре Base, и в эту строку нужно будет предварительно записать путь с подставленными аргументами — это задача для метода candles (и для каждого другого метода в генерируемых структурах) — там сейчас стоит многоточие, и мы его скоро раскроем.
Непосредственно скачивание данных выполняется вспомогательным классом MoexLink (файл MoexLink.mqh) — мы не будем его здесь рассматривать, т.к. он ожидаемо основан на WebRequest и правилах конструирования и обработки запросов протокола HTTP. Единственное, что стоит особенно отметить — в настройках класса предусмотрены режимы для сохранения получаемых из интернета данных в файлы на диске и последующее чтение данных из этого кеша вместо закачки при последующих запросах — таким образом облегчается диагностика ошибок и появляется возможность воспроизвести проблемную ситуацию под отладчиком. Дампы данных сохраняются в/загружаются из каталога /MQL5/Files/MOEX/today (если он существует) или /MQL5/Files/MOEX/YYYYMMDD (для текущей даты).
Кроме того, класс позволяет кастомизировать HTTP-заголовки и задавать постоянные параметры-опции, которые автоматически добавятся при каждом вызове WebRequest. В частности, при работе с ISS имеет смысл установить режим "iss.meta=off", чтобы не получать лишний блок описательных данных — его достаточно изучить один раз для каждого типа запроса.
Итак, вернемся к структуре Base. Получаемые от веб-сервиса данные (текст) парсятся в объект JSON и возвращаются в вызывающий код. Парсером выступает ToyJson (файл toyjson2.mqh) — расширенная версия того, что изначально было опубликовано в книге по алготрейдингу.
Таким образом, метод candles (и другие прикладные методы) должны будут иметь примерно следующий вид.
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; }
Функция GetRelation должна быть сгенерирована для проверки соответствия двух передаваемых аргументов (элементов перечислений, также генерируемых кодогенератором) друг другу. В данном примере проверяется, относится ли board к market, а market — к engine. В случае недопустимого сочетания должно быть выдано сообщение об ошибке и вернется пустая структура.
Макросы MOEX_REPx производят подстановку в строку templ фактических значений аргументов на места параметров запроса.
#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)
Все такие основополагающие вещи (базовые классы, макросы и пр.), на которые опираются генерируемые исходные коды, сведены в файл moexcore.mqh, написанный вручную.
В частности, в нем задекларировано перечисление MOEX_INTERVAL для таймфреймов, поддерживаемых ISS:
enum MOEX_INTERVAL { M1 = 1, M10 = 10, M60 = 60, H24 = 24, D7 = 7, D31 = 31, Q4 = 4 // NB: квартальный таймфрейм - отсутствует в MQL5! };
Обратите внимание, что в индексе — фрагмент показан ниже — они описаны под ключом "durations", но в параметрах запросов справочника называются "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К"] ] },
Разумеется, нам потребуется написать функцию для конвертации в родные таймфреймы MQL5 (за исключением квартального, для которого нет аналога).
Немного забегая вперед, скажем, что базовая структура Base поддерживает также постраничное скачивание данных в случае объемных запросов, кеширование динамически выделяемых объектов JSON в целях последующей сборки мусора (по запросу), а также анализ ответа на соответствие ожидаемым данным. Однако, последний пункт мы сможем реализовать чуть погодя.
Идентифицировать сущности разных типов из ISS удобно с помощью генерируемых перечислений, таких как MOEX_ENGINES, MOEX_MARKETS и т.д. Все они генерируются на основе индекса и выглядят следующим образом (на примере 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) Фондовый рынок и рынок депозитов };
Если вы сравните это определение с соответствующим разделом индекса, приведенным ранее, то заметите полное соответствие названий, значений и комментариев исходным именам, идентификаторам и описаниям.
Зная из индекса правила сочетания идентификаторов, можно сгенерировать функции проверки корректности пар, для чего кодогенератору достаточно создать массивы, например:
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); }
Здесь задействована универсальная функция CheckRelation, выполняющая проверку на наличие пары [pp,cc] в массиве ref, и она также вынесена в файл moexcore.mqh. Если старший по званию (левый) параметр равен 0 (что соответствует опущенному необязательному аргументу в запросе), функция сама выберет и вернет правильный идентификатор (элемент перечисления нужного типа).
OpenAPI
До сих пор мы рассматривали пути конечных точек, как самодостаточное описание функций, выполняемых веб-сервисом. На самом деле это не так. Стандартный HTTP-запрос содержит не только адрес сервера и путь на нем, но и параметры так называемой строки запроса — она задается в URL после знака '?'. Например,
https://iss.moex.com/iss/engines/stock/markets/shares/securities/AFLT/candles.json?from=2023-01-01&till=2024-01-01&interval=24
Слева от знака '?' идет путь, в котором, как мы знаем, могут указываться переменные участки (в спецификации выше помечены скобками) — мы их называли параметрами пути. Такие параметры определяет, к какому объекту на сервере мы обращаемся и какого типа сведения о нем хотим получить.
Справа от знака '?' следуют пары поименованных параметров "ключ=значение", соединенных знаком '&'. Такие параметры определяют, каким именно образом, в каком объеме, в каком формате передаются эти сведения.
В частности, в приведенном запросе свечей указан тикер "AFLT" на рынке акций, для которого дополнительно задается диапазон дат и D1-таймфрейм свечей.
Ранее мы приводили ссылку на спецификацию API ISS (https://iss.moex.com/iss/reference/) — она ведет на список конечных точек, который мы импортировали в файл reference.json. У каждой конечной точки есть собственный список параметров для строки запроса, и чтобы ознакомиться с ними, следует щелкнуть по соответствующей ссылке на главной странице. Вы также можете использовать прилагаемый файл ISS Queries Reference.html, где вся информация сведена воедино.
Кодогенератор, должен уметь подставлять в запрос не только параметры пути (что уже было описано), но и параметры строки запроса — специфические для каждого типа запроса.
Имеющаяся веб-страница хорошо подходит для восприятия человеком, но вычленение из неё структурированной информации для подачи в кодогенератор представляет довольно муторную задачу. Именно поэтому было бы желательно иметь описание API в формате OpenAPI. К счастью, на просторах интернета, а конкретно — GitHub, удалось найти неофициальный вариант OpenAPI для ISS, правда он устарел и неполон, а потому требует доработки. Исходный файл спецификации, сконвертированный из YAML в JSON, прилагается под именем moex-iss-api.json.
Мы не станем углубляться в формат OpenAPI. Скажем лишь, что на верхнем уровне документ имеет следующие разделы (ключи JSON):
- "openapi" — версия спецификации OpenAPI (их много, потому что OpenAPI также постоянно развивается);
- "info" — информация о веб-сервисе, описанном в данном конкретном файле;
- "servers" — адрес рабочих серверов сервиса, например, здесь указан https://iss.moex.com/ в случае ISS;
- "paths" — конечные точки сервиса;
- "security"— способ авторизации (для нас неважно, т.к. мы экспериментируем с публичной частью ISS);
- "components"— вся дополнительная информация, которая не укладывается в предыдущие разделы; в частности, здесь принято описывать структуры входных и выходных данных запросов;
Нас в первую очередь интересует раздел "paths".
Заглянем внутрь и посмотрим, что из себя представляет фрагмент, описывающий приведенный выше пример запроса свечей.
"/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
}
}
],
}
},
Массив "parameters" содержит все параметры запроса, причем их размещение помечено свойством "in", которое принимает значения "path" и "query" (бывают и другие, но здесь это неважно). Параметры пути ("path") мы можем выделить из самого пути, а вот параметры строки запроса ("query") мы видим только здесь. В частности, запрос свечей поддерживает:
- candles.start — для итеративного листания больших массивов свечей;
- candles.from — начальная дата интересующего диапазона дат;
- candles.till — конечная дата интересующего диапазона дат;
- candles.interval — таймфрейм;
- candles.iss.reverse — порядок сортировки свечей, прямой или обратный;
Кроме того, для каждого параметра дополнительно указывается тип данных — в свойстве "schema". К сожалению, в данном варианте спецификации все параметры имеют тип "string", что не обеспечивает строгую типизацию и проверку значений на стадии составления запроса программистом и пользователем. Всё это нужно для правильного форматирования параметров, без чего отправляемые запросы не будут обрабатываться сервером ожидаемым образом. Это следует исправить.
Для автоматического уточнения спецификации был написан вспомогательный скрипт moexOpenAPIedit.mq5. Его задача проставить правильные типы данных для всех параметров во всех запросах. Откуда мы возьмем информацию? Из "ручного" анализа текстового описания — если внимательно просмотреть справочник, становится ясно, что набор параметров ограничен парой десятков и его можно обобщить — выявить имена, которые используют только целые величины, имена для ввода дат и так далее. Однако это означает, что скрипт будет применим только к ISS и только к совместимым версиям OpenAPI.
Скрипт moexOpenAPIedit.mq5 имеет единственный входной параметр OpenAPIjson для указания редактируемого файла спецификации. В процессе работы будут созданы два json-файла: один с исходной спецификацией и второй с модифицированной — одинаковый стиль их форматирования облегчает контекстное сравнение при визуальном анализе изменений. Готовый файл с отредактированной спецификацией приложен к статье под именем moex-iss-api-mod.json.
Опишем в общих чертах, как он получен, и какова его роль в последующем процессе кодогенерации.
Очевидно, что параметры HTTP-запросов предполагают характерный тип данных. Например, параметры, задающие некие числовые диапазоны должны иметь тип integer, задающие даты и время — тип date (date-time), логические флаги-переключатели — boolean, и так далее (здесь указаны называния типов в нотации OpenAPI, а при переводе в MQL5 мы воспользуемся аналогичными типами MQL5 — long, datetime, bool).
Следует отметить, что по историческим причинам в ISS встречаются логические флаги двух видов: ожидающие значений true/false и целых чисел 1/0. Эти два вида придется по-разному обозначать в отредактированной спецификации — если для true/false подойдет boolean, то для второго вида будем дополнительно писать внутри "schema" свойство "format" : "int8".
Более того, поскольку мы предполагаем генерировать для каждой сущности ISS собственное перечисление (например, MOEX_ENGINES, MOEX_MARKETS и т.д.) мы можем более строго задать их в OpenAPI с помощью нестандартного свойства "x-enum-type", которое будет понимать лишь наш генератор. Так, для передачи в качестве параметра одной из торговых сессий будем писать внутри "schema" — "x-enum-type" : "MOEX_SESSIONS", где MOEX_SESSIONS — перечисление, формируемое кодогенератором (как ни странно, раздела сессий нет в индексном файле!).
Благодаря подробному и машиночитаемому описанию параметров в формате JSON, кодогенератор получает возможность возвращать из методов не "аморфную" структуру Moex::Entity, а структуру конкретного типа, учитывающую специфику соответствующего прикладного запроса. Например, для запроса свечей, который мы рассмотрели выше, кодогенератор создаст структуру Candles (показан реальный результат):
#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; } };
Наглядно видно, как описания параметров запроса из OpenAPI перекочевали практически в неизменном виде в определение структуры, включая и комментарии. Для каждого параметра описана переменная-член и одноименный метод-сеттер (начинается с _подчеркивания). Обратите внимание, что типы переменных строго задают ожидаемые от программиста и пользователя данные. Все методы-сеттеры возвращают копию самой структуры (this), что позволяет нанизывать их вызовы в цепочку, задавая сразу несколько аргументов.
Наконец, в методе _get, отправляющем запрос на веб-сервис, все непустые параметры собираются в массив query в виде правильно отформатированных строк (в соответствии с типами конкретных параметров, что обеспечивают макросы MOEX_STRING_XYZ из moexcore.mqh). Функция ArrayFindOrInsert добавляет аргументы в массив в алфавитном порядке, что обеспечивает канонический вид любого запроса — это важно для кеширования.
Каждое такое описание специфической структуры, как правило, используется в нескольких конечных точках — они упоминаются под нумерацией в верхней "шапке" комментариев. Каждая структура генерируется в отдельном файле с расширением ".inc", во вложенной папке MQL5/Files/MOEX/inc, и включается с помощью #include там, где нужно, в описание высокоуровневых структур:
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;
Заметьте, что теперь метод candles возвращает структуру Candles.
В результате пользователь библиотеки сможет настроить и выполнить запрос свечей в своем коде следующим образом:
JsValue *data = iss.engines.candles(Ticker, Board, Market, Engine)._interval(Timeframe)._from(From)._till(To)._start(MOEX_ALL)._get();
Или короче (потому что наша библиотека умеет автоматически определять рынок и движок по режиму торгов):
JsValue *data = iss.engines.candles(Ticker, Board)._interval(Timeframe)._from(From)._till(To)._start(MOEX_ALL)._get();
А если развивать эту идею дальше, то из спецификации инструмента можно автоматически узнавать и основной режим торгов.
Константа MOEX_ALL, определенная в moexcore.mqh как (-1), предписывает структуре Base скачать все страницы данных, если их окажется много.
Возвращаясь к фрагменту спецификации OpenAPI, приведенному выше, обратим внимание, что в названиях некоторых параметров стоит префикс "candles.". Это название блока данных в ответе сервера, на который эти параметры влияют. В данном случае, запрос свечей вернет лишь один блок данных со свечами, однако существуют запросы, в ответ на которые приходит несколько блоков, в частности, при запросе информации по тикеру получим, как правило, блоки "securities" (с описанием символа) и "marketdata" (с последними торговыми показателями), а также "dataversion" (с идентификаторами слепка данных). В принципе, существует способ исключить некоторые из блоков или запрашивать только конкретный блок в целях экономии ресурсов, но нас сейчас интересует другое.
Наличие префиксов позволяет реализовать в кодогенераторе проверку корректности получаемых данных. Например, если мы отправим запрос свечей, но в ответе сервера придет JSON без блока "candles", значит мы где-то допустили ошибку. Строки с проверкой генерируются в прикладных структурах вроде Candles — в примере выше они для простоты изложения опущены и заменены многоточиями. Желающие могут изучить подробности в прилагаемых исходных кодах.
Поскольку имеющаяся у нас спецификация в формате OpenAPI — неполная, кодогенератор сможет создать конкретные структуры не для всех конечных точек. Для случаев, когда определений не нашлось, в исходный код будет вставлен предупреждающий комментарий, а методы, выполняющие запросы, по-прежнему останутся завязанными на структуру Moex::Entity.
Реализация кодогенератора
Итак, мы имеем для подачи на вход кодогенератора следующие данные:
- индекс верхнеуровневых сущностей ISS, получаемый по запросу /iss/index (index.json);
- справочник запросов ISS, полученный конвертацией официальной веб-документации ISS (reference.json);
- неофициальная и неполная спецификация OpenAPI, которая дополняет справочник (moex-iss-api-mod.json);
Все файлы можно будет загружать в кодогенератор из интернета или с локального диска (имена образцов, поставляемых со статьёй, указаны в скобках), однако следует помнить, что только индекс предоставляется самим веб-сервисом, а две остальных конфигурации, при желании, следует подготовить и выложить на какой-либо сервер самостоятельно — тогда появится возможность удаленно корректировать (хоть и в ограниченном диапазоне) поведение кодогенератора в ответ на возможные изменения в сервисе.
Скрипт кодогенератора называется moexindexer.mq5. Указанные данные подаются на вход через соответствующие input-ы. Все файлы по умолчанию читаются и записываются в подкаталог MOEX.
input string MoexIssIndex = "MOEX/index.json"; input string MoexIssAPIReference = "MOEX/reference.json"; input string OpenAPIjson = "MOEX/moex-iss-api-mod.json";
Скрипт обрабатывает отдельно индекс, и отдельно — справочник вместе с опционально предоставляемой спецификацией в формате OpenAPI (если её не будет, синтаксис всех запросов потребуется выяснять самостоятельно и форматировать в единую строку без защиты от неправильного заполнения).
void OnStart()
{
ConvertIndex();
ConvertReference();
}
Обе ветви построены по одинаковому принципу — загрузка исходного файла, подготовка на его основе автогенерируемых исходных кодов и сохранение их в mqh-файле, который затем нужно будет подключить в свой проект работы с биржей. В частности, результат преобразования индекса записывается в файл moexindex.mqh (в случае запуска под отладчиком, файл получит имя 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); } } }
В функции MoexIndex2MQL5Converter (приводится с упрощениями), помимо ожидаемого парсинга текста в JSON, мы видим обработку ранее упоминавшегося дайджеста для выявления номеров колонок, через которые устанавливаются взаимосвязи между сущностями индекса.
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; }
Функция GenerateCommonEnums создает определения нескольких обобщенных перечислений (MOEX_ENGINES, MOEX_MARKETS, MOEX_BOARDS и т.д.) — именно они будут использоваться в демонстрационных программах далее.
Помимо этого, функция ConvertDigestLevel создает большое количество определений мелких, узкоспециализированных перечислений (например, MOEX_MARKETS_STOCK — для рынков движка stock, MOEX_BOARDS_STOCK_INDEX — для "досок" рынка index движка stock, и так далее по всей иерархии индекса, с учетом вложенности сущностей). Они пригодятся для альтернативных подходов кодогенерации (часть из них была теоретически рассмотрена во вводной части статьи), а также в самостоятельных проектах интеграции с биржей — особенно таких, которые предназначены только для конкретного рынка, например, фьючерсов или опционов. Все такие перечисления, вместе со связанными с ними input-ами, обложены директивами условной компиляции MOEX_DEMO_INPUTS и по умолчанию отключены.
В отношении альтернативных перечислений, стоит обратить внимание на следующий нюанс. Дело в том, что одно и то же слово часто используется для идентификации разных сущностей ISS: например, futures — это название и движка, и рынка, и группы режимов торгов, и типа ценных бумаг. Вместе с тем, в MQL5 все идентификаторы в перечислениях попадают в глобальное пространство имен, и потому в разных перечислениях не должно быть одноименных элементов. В связи с этим, элементы узкоспециализированных перечислений, там где необходимо избежать повторов, дополняются справа символом '_'.
Кроме того, во многих случаях идентификаторы составлены по принципу "имя_родителя·имя_элемента", где для соединения применяется символ '·' (dot — один из нескольких, разрешенных в идентификаторах, помимо латинских букв и цифр). Это сделано в целях унификации имен сущностей — чтобы они всегда содержали 2 компонента: контекст и имя элемента в нем. В ISS нет единства в данном принципе именования: в некоторых случаях названия элементов включают родительский контекст (например, "futures_spread" — календарный спред в контексте движка futures), а в некоторых нет ("option_on_commodities" — опцион на товары также в контексте движка futures, но упоминания futures здесь нет). Кодогенератор создает в таких случаях элемент перечисления вида "futures·option_on_commodities". Унификация имен позволяет упростить алгоритмическую обработку.
Аналогично индексу, справочник преобразуется в заголовочный файл moexref.mqh (или moexref-YYYYMMDDHHMM.mqh под отладчиком).
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 }
Функцию MoexReference2MQL5Converter, как и многие вспомогательные функции, оставим за рамками статьи. Внутри кодогенератора пришлось учесть множество нюансов ISS API и применить разрозненные алгоритмы.
Генерация основана на рекурсивном просмотре каждого пути по его фрагментам, начиная от корня, и аккумулировании тех или иных свойств элементов (идентификаторов, типов, перечислений, прототипов функций и т.д.) в нескольких массивах для последующего создания разнообразных конструкций в новом исходном коде.
Особенности реализации оставлены для самостоятельного изучения, включая и основные заголовочные файлы, требуемые для кодогенератора:
- moexlink.mqh — отправка и обработка веб-запросов;
- moexdigest.mqh — корневая структура индекса ISS в виде "дайджеста" (массив структур, описанный ранее);
- moexutils.mqh — вспомогательные функции;
- moexinit.mqh — ключевые слова MQL5 для разрешения конфликтов с именами ISS;
- toyjson2.mqh — парсер JSON;
Файлы moexindex.mqh и moexref.mqh, получаемые с помощью кодогенератора, приложены к статье. Отметим, что файл moexindex.mqh подключен в исходном тексте moexref.mqh, так что в прикладной проект нужно включать только один заголовок — moexref.mqh — он подключит все необходимые зависимости, в частности:
- moexcore.mqh — описание базовых структур, на основе которых работают сгенерированные коды;
- moex2mql5.mqh — конвертация данных ISS в прикладные структуры MQL5 (см. далее раздел прикладной интеграции);
- htmlstrp.mqh — упрощенный парсер HTML-страниц (см. далее раздел обработки ошибок);
После кодогенерации необходимо скопировать файлы moexref.mqh, moexindex.mqh и всю папку /inc из MQL5/Files/MOEX в папку вашего проекта.
Прикладная интеграция в MQL5
До сих пор мы отталкивались в разработке программ от организации и структур данных в спецификации ISS, а также от технических особенностей этого веб-сервиса. Сейчас настал момент соединить этот "мир" с "миром" MQL5.
Для библиотеки на основе автосгенерированного кода результатом успешного запроса к ISS является объект JSON, содержащий некие прикладные данные. Их конвертацией в родные для MQL5, встроенные структуры занимаются функции, сведенные в файл moex2mql5.mqh. Реализованы следующие задачи:
- перевод интервалов в таймфреймы;
- создание кастом-символа по спецификации из ISS;
- выяснение диапазона дат истории по символу;
- получение массива котировок MqlRates;
- получение массива тиков MqlTick;
- получение стакана цен как массива MqlBookInfo;
Перевод интервалов в таймфреймы выполняет функция Interval2Timeframe, но поскольку задача довольно тривиальна, мы опустим исходный код и пояснения.
Для полноценного создания кастом-символа желательно выполнить 2 запроса к ISS:
- /iss/securities/[security]
- /iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]
Первый возвращает расширенное описание символа (блок "description") и перечень режимов торгов для него ("boards"), второй — обобщенные результаты торгов ("securities") и текущую рыночную информацию ("marketdata"). Хотя многие поля пересекаются, есть и исключения, которые дополняют друг друга, в особенности для различных рынков. Поэтому два объекта JSON имеет смысл объединить, и уже на основе агрегированной информации создавать кастом-символ. Как мы увидим далее в примере, объединить объекты JSON позволяет метод merge из библиотеки ToyJson2. Далее такую скомпонованную информацию готова принять функция Moex::CreateCustomSymbol из moex2mql5.mqh — JSON поступает первым параметром 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; }
Символ создается в папке MOEXISS, во вложенных папках по именам движка и рынка. Для выяснения валюты следует в первом из двух обсуждаемых запросов установить дополнительный параметр "lang=en", т.к. в противном случае, на русском языке, содержимое свойства "unit" придет в свободной форме, вроде "доллар США" вместо "USD".
Использованные в CreateCustomSymbol вспомогательные функции не поясняются в статье — смотрите исходные коды.
Глубину истории баров по инструменту можно узнать запросом вида:
/iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/candleborders
Он вернет данные с секцией "borders", в которой для каждого таймфрейма указаны начальная и конечная даты.
Такой объект JSON можно преобразовать в понятный для MQL-программы вид функцией 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; }
Используемый здесь класс ColumnVerificator проверяет наличие требуемых столбцов (свойств) в JSON, а также запоминает индексы соответствующих столбцов, чтобы в дальнейшем считывать данные по номеру (каждый порядковый номер равен значению элемента перечисления Columns). Этот класс применяется и в других функциях конвертации данных.
В предыдущих разделах мы уже видели примеры запросов свечей — они возвращают JSON с секцией "candles". Для его превращения в массив MqlRates предназначена функция 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; }
Похожим образом обрабатываются сделки, которые можно получить от ISS с помощью запроса вида:
/iss/engines/[engine]/markets/[market]/boards/[board]/securities/[security]/trades.json
Сделки — это тики в терминологии MetaTrader 5. За конвертацию JSON в тики отвечает класс Trades2Ticks, а точнее — его метод 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; } };
В данном случае, перечень колонок может отличаться в зависимости от рынка, поэтому класс предусматривает параметризацию ColumnVerificator произвольным перечислением. По умолчанию предоставляется перечисление, которое включает обобщенный набор наиболее часто встречающихся и востребованных колонок. В частности, колонки tradeno или recno содержат уникальный номер тика, с помощью указания которого в параметрах можно запрашивать у ISS более новые тики — мы продемонстрируем это в примере в выделенном разделе статьи.
Следует обратить внимание, что из потока сделок мы можем узнать только одну цену — last, а для дополнения структуры MqlTick текущими ценами ask/bid потребуется выполнить какой-либо другой запрос к ISS — в идеале это должен быть "стакан", но он доступен только по подписке, поэтому мы воспользуемся другим запросом:
/iss/engines/[engine]/markets/[market]/boards/[board]/securities.json?securities=[security]
В ответ на него приходят данные, имеющие, в частности, секцию "marketdata", а в ней — нужные нам столбцы "offer" и "bid". Это также найдет отражение в примере.
Для конвертации "стаканов" предусмотрена функция Moex::GetOrderbook, похожая на вышеприведенные:
bool GetOrderbook(JsValue *data, MqlBookInfo &book[], const bool reverify = false);
Важно отметить, что запросы "стаканов" возвращают их текущие слепки, причем с периодичностью не чаще, чем позволяют канальные затраты времени вашего соединения с веб-сервисом. Для получения истории "стаканов" можно самостоятельно архивировать слепки, либо рассчитывать "стаканы" по истории ордеров (по подписке, через запросы группы /iss/archives).
Примеры применения
Продемонстрируем на практике, как работает библиотека сгенерированных кодов.
Для всех примеров важно разрешить в настройках терминала доступ к домену iss.moex.com.
У всех примеров имеется общий набор параметров для настройки работы класса 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)); ... }
По умолчанию, подробные логи отключены (Logging), но все полученные данные складируются в дамп (Dumping) на случай отладки. Если такая необходимость возникнет, нужно будет поставить Test = true — в этом режиме имеющиеся в дампе ответы на сделанные ранее запросы будут загружаться с диска, а не из интернета. Между двумя соседними запросами делается задержка (Delay) 100мс, чтобы не сильно нагружать сервер, когда скачивается длинная история или большой справочник инструментов. Строка Parameters предназначена для установки постоянных дополнительных опций во всех запросах, в частности, таким образом по умолчанию отключается секция "metadata" (полезная только на этапе ознакомления с конкретным запросом).
Скрипт moexmarket.mq5 позволяет посмотреть обобщенную информацию о выбранном рынке, а также выполнить контекстный поиск по строке или по фильтру ценных бумаг.
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)); } } }
Скрипт moexsymbol.mq5 дает возможность создать кастом-символ по выбранному тикеру ISS и обновляет его бары и тики онлайн. Для правильного формирования кастом-символа оставьте таймфрейм M1, прописанный по умолчанию — MetaTrader 5 должен сам рассчитать бары старших таймфреймов. Следите за тем, чтобы тикер относился к выбранному рынку (или сбросьте рынок и движок, чтобы программа выбрала их автоматически по режиму торгов).
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(""); } }
Результат работы для AFLT показан на следующем скриншоте.
Трансляция тиков кастом-инструмента с биржи
А вот как выглядят спецификации импортированных фьючерсов.
+
Спецификации кастом-инструментов по данным биржи
Обработка ошибок
При взаимодействии с веб-сервисом могут возникать ошибки разного рода, препятствующие получению ожидаемых данных, но проявляющие себя по-разному. Часть ошибок (низкоуровневая или транспортная) перехватывается на уровне вызова WebRequest и приводит к взведению флага _LastError — результат запроса при этом пустой, и такие ситуации должны каким-либо образом обработаться вызывающим кодом — например, путем выдачи предупреждения пользователю (код ошибки пишется классом MoexLink в лог при включенном режиме журнала). Другая часть ошибок (высокоуровневая или прикладная) характеризуется наличием ответа на запрос, но в принятом HTTP-заголовке, как правило, проставлен HTTP-статус, отличный от 200 (успех). Например, это может быть код 404 (страница не существует) или 500-й серии (проблемы на сервере). В подобных случаях веб-сервис возвращает HTML-страницу, а не сообщения в запрошенном формате (в нашем случае — всегда JSON).
В связи с этим, в библиотеке реализован упрощенный алгоритм выделения текста из HTML-страниц (см. htmlstrp.mqh), который запускается в случае проблем с парсингом ответа в JSON. Такой подход предоставляет диагностическую информацию для пользователя.
К сожалению, ошибки прикладного уровня иногда случаются там, где не должны, а иногда они сопровождаются HTTP-статусом успеха (200). Приведем пару иллюстраций сказанного.
Примеры сообщений об ошибках в запросах: верх — отказ в доступе к orderboook (http-код: 200, т.е. формально успех),
низ — формально верный, но неработающий запрос через boardgroups (http-код: 404)
При разработке своих программ будьте готовы к тому, что веб-сервис ISS функционирует не совсем так, как описано.
Сопровождение
Напомним, что работа библиотеки основана на справочнике API и индексе ISS. И если первый меняется относительно редко — обычно при апгрейде версии API, то второй — достаточно часто, может быть, каждый день. К счастью, изменения индекса, как правило, заключаются в переключении различных режимов между состояниями вкл/выкл, что отображается в данных как замена 0 на 1 и обратно. Списки объектов индекса — их названия, идентификаторы и иерархия подчинения — при этом остаются неизменными, то есть однажды сгенерированный исходный код библиотеки должен оставаться работоспособным.
Однако, для надежности, рекомендуется в начале каждого дня скачивать новый индекс и сравнивать его с предыдущим на предмет значительных изменений.
Напомним, что полный индекс ISS (все сущности верхнего уровня, без ценных бумаг) можно получить с помощью запроса /iss/index, который реализуется в сгенерированном коде следующим образом:
JsValue *index = iss.index.index()._get();
В целях упрощения контекстного сравнения двух версий индекса, к статье прилагается скрипт jsoncmp.mq5, который анализирует два JSON-файла путем их парсинга в объекты JSON и дальнейшего применения к ним метода match из ToyJson2. Данный скрипт рассматривает изменения целочисленных свойств 1 -> 0 и 0 -> 1 несущественными. Любое другое отличие вызовет предупреждение. Предполагается, что пользователь должен оценить изменения лично и, при необходимости, перегенерировать библиотеку.
Заключение
В данной статье мы рассмотрели внутреннюю организацию и технические особенности веб-сервиса биржи, реализовали средства автоматической генерации кода универсальной "обертки" для всего публичного API.
На основе представленных структур и примеров можно реализовать широкий спектр аналитических инструментов, объединяющих в себе показатели экспертного уровня (поступающие с биржи) и технический функционал обработки и визуализации данных MetaTrader 5.
Предложенный инструментарий, вероятно, можно использовать для интеграции с API других бирж, но, скорее всего, потребуется его адаптация или даже существенная переработка.
Описание файлов, используемых в статье
Каталог/Файл | Описание |
---|---|
MQL5/Scripts/MOEXISS | |
htmlstrp.mqh | Средство выделения текста из HTML-страниц |
inc | Пример папки с автоматически сгенерированными структурами, включенными в moexref.mqh |
ISS Queries Reference.html | Полная веб-страница со спецификацией ISS |
jsoncmp.mq5 | Скрипт сравнения двух JSON-файлов |
jstraverse.mqh | Вспомогательный класс обхода JSON-объекта |
moex2mql5.mqh | Функции прикладной интеграции данных из ISS в MQL5 |
moexcore.mqh | Базовая структура для MQL5-обертки веб-сервиса ISS |
moexdigest.mqh | Организационная реляционная таблица сущностей ISS для moexindexer.mq5 |
moexindex.mqh | Пример автоматически сгенерированного кода, отображающего индекс ISS в MQL5 |
moexindexer.mq5 | Скрипт автогенерации MQL5-обертки веб-сервиса ISS |
moexinit.mqh | Вспомогательный загрузочный код для moexindexer.mq5 |
moexlink.mqh | Отправка и обработка HTTP-запросов |
moexmarket.mq5 | Пример получения информации о рынке из ISS, на основе MQL5-обертки ISS |
moexopenapiedit.mq5 | Скрипт дополнения спецификации ISS в формате OpenAPI |
moexref.mqh | Пример автоматически сгенерированного кода, отображающего спецификацию ISS в MQL5 |
moexsessions.txt | Описание сессий, дополняющее индекс ISS в moexindexer.mq5 |
moexsymbol.mq5 | Пример создания и обновления кастом-символа с запросом спецификаций, баров и тиков из ISS |
moexutils.mqh | Вспомогательные функции |
reserved.txt | Список ключевых слов MQL5 для moexindexer.mq5 |
StringUtils.mqh | Строковые функции |
toyjson2.mqh | Реализация JSON |
MQL5/Include/MQL5Book | |
AutoPtr.mqh | Класс автоуказателя |
Defines.mqh | Общеупотребительные макроопределения |
EnumToArray.mqh | Декодирование перечисления в массив |
MapArray.mqh | Ассоциативный массив |
MqlError.mqh | Сообщения об ошибках MQL5 |
PRTF.mqh | Отладочный вывод выражений в лог |
MQL5/Files/MOEX | |
index.json | Индексный файл сервиса ISS (для кодогенератора) |
reference.json | Список конечных точек сервиса ISS (для кодогенератора) |
moex-iss-api-mod.json | Неофициальная спецификация ISS в формате OpenAPI (улучшенная, для кодогенератора) |
moex-iss-api.json | Неофициальная спецификация ISS в формате OpenAPI (исходная, для сведения) |
index-new.json | Пример индексного файла с изменениями (для проверки сравнения JSON-файлов) |





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Небольшой нюанс, опущенный в статье.
Если серверное время вашего МТ5 отличается от московского и предполагается совместно анализировать данные с биржи и от вашего брокера, то все значения дата-время от биржи (в котировках, тиках и т.д.) нужно корректировать на разницу часовых поясов. Например, для центральноевропейского сервера при текущем зимнем времени нужно вычитать 1 час из получаемого времени с биржи.
Конкретно через ISS API торговать не предусмотрено. В начале статьи перечислены некоторые (но не все) протоколы, которые биржа предлагает для торговли - разумеется, не бесплатно. Про их особенности можно почитать на сайте самой биржи. Их можно подключить к МТ5 разными способами - это отдельная большая работа. Полагаю такого рода статью потенциально может написать кто-то, кто биржевым программированием уже занялся, я пока остановился на ISS.
Если нужно что-то близкое к HFT, то, вероятно, лучше действительно пилить какую-то свою программулину (хотя бы библиотеку), потому что событийная модель МТ5 не позволяет получать данные в реальном времени (то есть в виде "пуша" по инициативе биржи, а не зацикленного "пула" от МТ5, но вроде я где-то видел API биржи на веб-сокетах - не вдавался в подробности).
Предполагаемый способ применения текущей связки - обработка данных биржи в МТ5 с помощью имеющихся индюков и отбраковка стратегий/оптимизация экспертов на кастом-символах. На основе этой инфы можно торговать в терминалах других брокеров.
Иными словами - брокер или подключение к бирже по другим протоколам (API) необходимо для торговли.
Самый простой вариант - инструменты биржи в МТ5 от брокера, но выбора тут нет (сейчас).