Помимо международных стандартов и протоколов передачи финансовой информации вроде FIX и FAST, о которых мы рассказывали ранее, на фондовом рынке функционируют и так называемые «нативные» протоколы передачи финансовых данных. Их используют для получения нужной информации как частные трейдеры, так и брокерские компании — такие нативные протоколы более функциональны, чем общепринятые стандарты (вроде того же FIX), что привлекает брокеров.
Ранее в России существовали две крупные биржи — ММВБ и РТС. Впоследствии они объединились в единую «Московскую биржу», но каждая из двух торговых площадок за годы независимости успела разработать собственный нативный протокол. О протоколе Plaza II, который был создан специалистами РТС, мы рассказывали в одном из прошлых материалов, а сегодня речь пойдет о проекте ASTS Bridge, который начали развивать их коллеги из ММВБ.
Немного истории
Первая версия электронной торговой системы Московской Межбанковской Валютной Биржи (ММВБ) была разработана и внедрена в 1993-1994 годах прошлого века. Система получила название ASTS (Automated Securities Trading System) и ее программная часть была разработана австралийской компанией FMSC (Financial Market Software Consultants) и адаптировано для российского рынка совместными усилиями зарубежных и отечественных ИТ-специалистов.
Позднее FMSC в ходе слияний и поглощений получила название Compu ShareLtd с которой ММВБ подписала партнерское соглашение, предусматривающее в том числе дальнейшее самостоятельное развитие торговой системы.
Этапы развития торговой системы ММВБ до середины 2000-х годов; источник: micex.ru
Далее система развивалась силами специалистов биржи ММВБ, а позднее «Московской биржи». В итоге была реализована трехуровневая архитектура клиент-серверной системы.
Во главе иерархии находился центральный сервер торговой системы (он отвечает за обработку транзакций), с которой взаимодействовали серверы доступа (на них реплицировались все транзакции торговой системы), к которым в свою очередь подключались клиентские приложения (торговые терминалы трейдеров и администраторов, брокерские торговые системы, комплексы распространения биржевой информации и т.п.):
Источник: документация ММВБ
В такой конфигурации система существует с 1998 года.
Передача данных: используемые протоколы
Для подключения к торговой системе внешних систем был разработан механизм универсального двунаправленного программного шлюза (УДПШ) — то есть софта, с помощью которого осуществлялся обмен данными между торговой системой биржи и подключаемыми к ней приложениями.
ASTS Bridge
Программа обеспечивала двунаправленную связь с торговой системой и имела API для получения данных (сделки, котировки, инструменты и т.п.) и выполнения транзакций (постановка/снятие заявок и т.п.).
Существовало две версии УДПШ — TCP/IP-версия системы называлась TEAP, а вариант для подключения с помощью последовательного интерфейса (RS-232) назывался TEServer (Trade Engine Server). С лета 2015 года поддержка этой версии прекращена.
Впоследствии УДПШ получил новое название — ASTS Bridge. В терминологии Биржи шлюз (bridge) — это нативный протокол торгово-клиринговой системы,
Особенностью шлюзового протокола является поддержка так называемых «интерфейсов». Как сказано в материале «Московской биржи» на Хабрахабре:
Интерфейс – это имеющий версию набор доступных пользователю таблиц и транзакций, с соответствующей структурой и типами данных.
Версионность позволяет пользователям системы оставаться на старых версиях интерфейсах после обновлений биржевой системы, которые влекут за собой необходимость модификации структуры таблиц данных или изменения форматов транзакций. В настоящий момент существует возможность подключения всеми версиями интерфейсов, созданными за все годы работы системы, однако планируется ужесточение требований к ним.
Подробная документация по ASTS Bridge представлена на FTP «Московской биржи». Среди прочего там есть и описания существующих интерфейсов.
Как это работает
В настоящий момент торгово-клиринговая система ASTS «Московской биржи» обеспечивает функционирование основных рынков торговой площадки и поддерживает несколько методик осуществления торгов — например, встречный аукцион (Order-Driven Market) и торговля котировками (Quote-Driven Market). Подробнее о функциональности системы можно прочитать на сайте Биржи.
Серверная часть клиентского приложения устанавливается на сервере, подключенном к закрытой торговой сети биржи, а клиентская часть запускается на компьютере, подключенном к сети клиента. Также существует возможность размещения торгового приложения клиента на колокации в дата-центре биржи М1. В таком случае разрешается использование так называемой встроенной версии шлюза, которая позволяет подключаться напрямую к биржевым серверам доступа.
Торговое приложение клиента должно использовать предоставляемые API-функции для получения данных из системы ASTS и выполнения транзакций. Данные получаются по технологии client pull — приложение должно само «вытягивать» данные, то есть опрашивать таблицы для получения актуальной рыночной информации.
Таким образом, для старта работы, разработчику торгового приложения нужно скачать шлюз ASTS Bridge на сайте Биржи, а затем подключить его к тестовой версии ASTS. По завершению разработки и отладки каждое приложение, подключаемое к системе проходит сертификацию Биржей.
После прохождения всех этих этапов схема работы приложения с системой ASTS выглядит следующим образом.
- Сначала происходит установка соединения и авторизация участника торгов, а затем из торгово-клиринговой системы запрашивается структура информационных объектов и необходимые для работы с таблицами данные — ответ приходит в виде буфера данных.
- Затем этот буфер распаковывается для разделения табличных данных на строке и, затем, на поля.
- Если таблицы являются обновляемыми (на это указывает специальный флаг), задается интервал и последовательность запроса обновленных данных из системы.
- При выполнении внешним клиентским приложением транзакций, состав полей таких транзакций также формируется на основе структуры информационных объектов.
В приложении должны быть предусмотрены механизмы восстановления состояния открытых таблиц после сбоев и потерь связи — без этого получить сертификат для последующей эксплуатации приложения не удастся.
Ниже представлен пример реализации демо-приложения на Java, подключающегося и запрашивающего данные из системы ASTS:
import com.micex.client.API.ServerInfo;
import com.micex.client.Binder;
import com.micex.client.Client;
import com.micex.client.ClientException;
import com.micex.client.Filler;
import com.micex.client.Meta;
import com.micex.client.Parser;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Demo implements Binder {
public static void main(String[] args) throws ClientException {
final Map<String,String> m = new HashMap<String, String>();
m.put("PacketSize","60000");
m.put("Interface","IFCBroker_20");
m.put("Server","INET_GATEWAY");
m.put("Service","inet_gateway");
m.put("Broadcast","91.208.232.101");
m.put("PrefBroadcast","91.208.232.101");
m.put("UserID", "MU0000800001");
m.put("Password", "");
m.put("Language", "English");
new Demo().run(m);
}
public void run(Map<String,String> parameters) throws ClientException {
Client client = new Client();
client.start(parameters);
try {
// Some useful info about connection
System.out.println(String.format("Connected to MICEX, handle=%d", client.handle()));
final ServerInfo info = client.getServerInfo();
System.out.println(String.format("SystemID=%s; SessionID=%d; UserID=%s",
info.systemID, info.sessionID, info.userID));
// Parsed market interface, contains meta-information
// about available requests (tables) / transactions
// and their structure definition.
final Meta.Market market = client.getMarket();
System.out.println(String.format("Market: %s", market.name));
// Optional MTESelectBoards
final Set<String> b = new HashSet<String>();
b.add("TQBR"); // Use only one - limit number of SECURITIES in demo
client.selectBoards(b);
Parser parser;
// load() table - mimics a sequence of MTEOpenTable/MTECloseTable.
// Use it for non-updateable info-requests
parser = client.load("MARKETS", null);
parser.execute(this);
// open() table - it will also be added to the list
// of requests to be updated at refresh() call
parser = client.open("TESYSTIME", null, true);
parser.execute(this);
// open() SECURITIES (params==null - all securities)
parser = client.open("SECURITIES", null, false);
parser.execute(this);
// another (better) way to specify table name
Meta.Message orderbooks = market.tables().find(Meta.TableType.Orderbooks);
if (orderbooks != null) {
parser = client.open(orderbooks.name, null, false);
parser.execute(this);
}
// make 10 refresh() iterations with some delay between them
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
return;
}
parser = client.refresh();
if (parser.empty()) continue; // nothing to parse, skip the rest
int bytes = parser.length();
int count = parser.execute(this);
System.out.println("====================");
System.out.println("Parsed " + bytes + " bytes, " + count + " rows");
}
} finally {
client.close();
System.out.println("====================");
System.out.println("Done.");
}
}
/**
* Very simplistic table storage.
*/
final Map<String, Table> database = new HashMap<String, Table>();
/**
* @param source - a TE request which is gonna be parsed
* @return - a Filler instance which will be used to
* store parsed values in some kind of storage or {@code null}
* if not interested in storing parsed data.
*/
public Filler getFiller(Meta.Message source) {
// Of course, IRL you shouldn`t return a new instance
// of Filler instance, but search some kind of
// internal database and return the same "table"
// instance for every source message.
Table table = database.get(source.name);
if (table == null) {
table = new Table();
database.put(source.name, table);
}
return table;
}
/**
* Very simplistic record.
*/
static class Record {
int decimals;
final Map<String,Object> values = new LinkedHashMap<String, Object>();
}
/**
* Very simplistic table.
*/
static class Table implements Filler {
/**
* Записи, хэшированные по значению первичного ключа (если есть)
* или по порядковому номеру при отсутствии ключевых полей.
*/
final Map<String, Record> records = new HashMap<String, Record>();
/**
* Специальная структура для хранения "стаканов" - блоки записей,
* хешированные по коду инструмента (SECBOARD + SECCODE).
*/
final Map<String, List<Record>> orderbooks = new HashMap<String, List<Record>>();
public boolean initTableUpdate(Meta.Message table) {
// Парсер начинает обрабатывать буфер ответа.
// Здесь также при необходимости нужно провести
// обработку в зависимости комбинации значений
// table.isClearOnUpdate() table.isOrderbook();
if (table.isClearOnUpdate())
records.clear();
return true; // Начиная с 1.1.0 возвращаемое значение игнорируется.
}
public void doneTableUpdate(Meta.Message table) {
// Просто окончание работы парсера, а-ля commit.
// Очистка более ненужных переменных.
orderbook = null;
}
final Map<String, Object> keys = new LinkedHashMap<String, Object>();
public void setKeyValue(Meta.Field field, Object value) {
// Задает значения ключевых полей.
// В демо-примере - собираем значения в map.
// По окончании обработки записи собранные значения следует
// сбросить (см. doneRecordUpdate())
keys.put(field.name, value);
}
Record current;
List<Record> orderbook;
public boolean initRecordUpdate(Meta.Message table) {
// Начало обработки записи.
// Здесь нужно произвести поиск по ключам
// и вернуть true, если запись не найдена (т.е. новая)
if (table.isOrderbook()) {
// Для таблиц типа "orderbook" - специальная обработка
// Запись всегда будет новой, добавим ее в "стакан"
current = new Record();
orderbook.add(current);
return true;
} else {
System.out.println("Table:" + table.name +"; keys: " + keys.toString());
if (keys.isEmpty()) {
// setKeyValue() ни разу не был вызван -
// данная таблица не имеет первичных ключей.
// Все записи будут считаться новыми, хранить
// их будем по порядковому номеру.
current = new Record();
records.put(Integer.toString(records.size()), current);
return true;
} else {
// У таблицы есть первичные ключи -
// ищем и храним записи по их значению.
final String key = keys.toString();
current = records.get(key);
if (current == null) {
// Запись по ключу не найдена - создадим новую
current = new Record();
records.put(key, current);
return true;
}
// Запись была найдена
return false;
}
}
}
public void setRecordDecimals(int decimals) {
// Парсер дли новой записи определил
// кол-во десятичных знаков - сохранить (в записи).
current.decimals = decimals;
}
public int getRecordDecimals() {
// Парсеру для работы понадобилось знать
// о кол-ве десятичных знаков по найденной
// ранее записи - отдать сохраненное.
return current.decimals;
}
public void setFieldValue(Meta.Field field, Object value) {
// Парсер задает значение конкретного поля записи
current.values.put(field.name, value);
}
public void doneRecordUpdate(Meta.Message table) {
// Закончили обрабатывать запись.
// Хорошее место для сохранения накопленных данных куда-то.
// В демо-примере - просто печать в консоль.
System.out.println("Table:" + table.name +"; data: " + current.values.toString());
// Здесь также следует сбросить собранные значения первичных ключей,
// чтобы подготовиться к получению значений для следующей записи.
keys.clear();
current = null;
}
public void switchOrderbook(Meta.Message table, Meta.Ticker ticker) {
// Специфическая операция для таблиц типа "котировки" (table.isOrderbook())
// Для таких таблиц значения ключевых полей setKeyField() не задаются для
// каждой записи, вместо этого идет "переключение стакана" - блока записей.
// Данный вызов информирует вас о том, что начинается новый блок
// для указанного инструмента ticker.
orderbook = orderbooks.get(ticker.toString());
if (orderbook == null) {
// Инструмент встретился впервые,
// подготовм для него "стакан"
orderbook = new ArrayList<Record>();
orderbooks.put(ticker.toString(), orderbook);
} else {
// "Стакан" уже есть - его нужно очистить,
// т.к. новые значения полностью заменяют старые.
orderbook.clear();
}
}
}
}
Важный момент: несмотря на то, что пример выше на Java, на самом деле для работы с протоколом ASTS Bridge можно использовать любые языки. Никаких ограничений для разработчиков торговых систем в этом плане нет.
Заключение
По данным представителей «Московской биржи» на июнь 2015 года, собственный нативный протокол пока уступает по популярности среди трейдеров протоколу FIX. На фондовом рынке на FIX приходится до 60% заявок.
Тем не менее, у ASTS Bridge есть и свои плюсы, например единство информационных объектов (например, таблиц и транзакций), которые являются одинаковыми для всех рынков, что облегчает адаптацию торговых приложений на работе на каждом из них. Еще одним плюсом ASTS Bridge можно назвать низкие системные требования — для работы по этому протоколу не требуется наличие выделенного сервера, клиентское приложение может быть запущено даже на персональном компьютере.
Для работы по этому и другим описанным в наших предыдущих статьях протоколов для прямого доступа на биржу, необходимо заключить договор с брокером (например, ITinvest), который помогает организовать доступ к торгам по выбранной технологии.
На сегодня все, спасибо за внимание, будем рады ответить на вопросы в комментариях.
В наших следующих статьях мы продолжим рассказывать о существующих биржевых технологиях, в частности, речь пойдет о протоколе Simple Binary Encoding, который, в определенной степени, является продолжателем дела FIX.