Fix руководство запуска

Время на прочтение
15 мин

Количество просмотров 8.9K


В предыдущей статье мы использовали приложение MiniFIX для подключения и отправки сообщений на тестовую биржу с помощью протокола FIX. В этой статье напишем собственную реализацию клиента для получения рыночных данных в виде небольшого SpringBoot-приложения. Код доступен в репозитории.

Для реализации приложения нам понадобится:

  • Java 8
  • Maven
  • Spring boot 2.2.5
  • Lombok
  • QuickFix/J

Содержание для упрощения навигации по статье:

  • FIX-Engine и запуск тестового сервера
  • Структура проекта
  • Настройка параметров подключения
  • Создание FIX-приложения
  • Создание сервиса для подключения к серверу
  • Отправка запроса на получение рыночных данных
  • Обработка ответа и сохранение рыночных данных
  • Запуск приложения

FIX-Engine и запуск тестового сервера

FIX-Engine, или FIX-движок, обеспечивает связь со сторонними системами по протоколу FIX. Он отвечает за преобразование данных в FIX-сообщения, а также за создание сессии и обеспечение ее работы: проверку валидности сообщений, генерацию контрольных сумм, восстановление работы после потери связи и т.д (здесь можно почитать более подробно).

В нашем случае в роли такого движка выступает QuickFix/J. В предыдущей части я использовала пример Executor из модуля examples, но в нем обрабатываются только сообщения на создание торговых заявок. В этом же модуле есть более подходящий пример — OrderMatch (quickfixj-examples-ordermatch), в нем помимо поддержки торговых заявок присутствует обработка сообщений на получение рыночных данных (MarketDataRequest).

Когда вы первый раз клонируете репозиторий, обязательно нужно выполнить сборку проекта, чтобы сгенерировались FIX-сообщения для различных версий протокола. В Readme проекта есть описание команд для различных видов сборки (с тестами и без), самый быстрый:

mvn clean package -Dmaven.javadoc.skip=true -DskipTests -PskipBundlePlugin

Процесс сборки длился у меня где-то минут 6-7, так что в это время можно заварить себе чашечку чая изучить настройки сервера и приступить к написанию клиента.

Как я уже описывала ранее, открываем файл resources/quickfix.examples.ordermatch/ordermatch.cfg, проверяем SocketAcceptPort и заполняем поле TargetCompID нужным значением для нашего клиента (можно оставить BANZAI, которое указано по умолчанию, можно написать любое другое на ваше усмотрение):

[default]
FileStorePath=target/data/ordermatch
DataDictionary=FIX42.xml
SocketAcceptPort=9876 // порт для подключения
BeginString=FIX.4.2 // версия FIX 4.2

[session]
SenderCompID=EXEC // идентификатор сервера
TargetCompID=FIX_CLIENT // идентификатор клиента
ConnectionType=acceptor
StartTime=00:00:00
EndTime=00:00:00

Если хотите поменять значение идентификатора клиента, то лучше, конечно, сделать это перед сборкой, чтобы не пришлось собирать еще раз.

Когда сборка завершится, заходим в quickfixjquickfixj-examplesordermatchtarget, проверяем, что там появились *.jar файлы:

Запускаем файл quickfixj-examples-ordermatch-2.2.0-SNAPSHOT-standalone.jar, так как он содержит все необходимые для запуска зависимости:

java -jar quickfixj-examples-ordermatch-2.2.0-SNAPSHOT-standalone.jar

Если появилась запись «Started QFJ Message Processor» – значит, сервер запустился. Проверьте, что в строке «Listening for connections at … [FIX4.2:EXEC->FIX_CLIENT]» указано нужное значение идентификатора клиента.

Структура проекта

Вот так выглядит готовый проект (стандартная структура веб-приложений: сервисы, контроллеры, модельки и т.д):

Создаем maven-проект со стандартными зависимостями и добавляем библиотеку QuickFix/J для работы с протоколом FIX:

<properties>
   <quickfixj.version>2.0.0</quickfixj.version>
</properties>
<dependency>
   <groupId>org.quickfixj</groupId>
   <artifactId>quickfixj-core</artifactId>
   <version>${quickfixj.version}</version>
</dependency>

<dependency>
   <groupId>org.quickfixj</groupId>
   <artifactId>quickfixj-messages-fix42</artifactId>
   <version>${quickfixj.version}</version>
</dependency>

Я подключила 2 модуля: quickfixj-core и quickfixj-messages-fix42 для работы с сообщениями только версии FIX 4.2.

Если в вашем приложении предполагаются сообщения различных версий протокола, можно подключить quickfixj-core + quickfixj-messages-all или просто quickfixj-all.

Полная версия pom.xml доступна в репозитории.

Настройка параметров подключения

По аналогии с файлом настроек на сервере, создадим файл resources/config/client.cfg с настройками нашего приложения.

В файле может быть один блок [default], в котором находятся параметры, общие для всех сессий, и несколько блоков [session] для описания параметров конкретной сессии (если сервер поддерживает сообщения различных версий протокола FIX, то для каждой версии создается отдельный блок [session]).

[default]
SenderCompID=FIX_CLIENT // идентификатор отправителя
TargetCompID=EXEC // идентификатор получателя
ConnectionType=initiator // приложение является клиентом
NonStopSession=Y
SocketConnectHost=localhost
ReconnectInterval=5
HeartBtInt=30
FileStorePath=target/data/banzai
UseDataDictionary=Y
DataDictionary=dictionary/fix4_2.xml
ValidateUserDefinedFields=N
AllowUnknownMsgFields=Y
ValidateUserDefinedFields=N
AllowUnknownMsgFields=Y

[session]
BeginString=FIX.4.2
ResetOnLogon=Y

Подробнее о параметрах

Начнем с блока [default]:

  • параметры сессии
    SenderCompID, TargetCompID – идентификатор отправителя и получателя сообщений соответственно (sender – наше приложение, target – сервер). Убедитесь, что эти значения совпадают со значениями параметров на сервере.
    ConnectionType (initiator/acceptor) – указывает, является наше приложение клиентом или сервером.
    — С помощью параметров StartTime и EndTime можно указать время начала и соответственно завершения работы сессии (например, биржа работает с 9.00 до 18.00, поэтому нет смысла запускать сессию вне этого времени). Если же сессия будет работать весь день, то можно указать NonStopSession=Y, что будет равносильно варианту StartTime=00:00:00 и EndTime=00:00:00.
    -параметры валидации сообщений
    UseDataDictionary=Y – можно использовать словарь сообщений, если вы работаете с биржей, спецификация сообщений которой отличается от стандартной (например, в словаре можно указать дополнительные теги или типы сообщений). При этом использование словаря обязательно, если есть сообщения с повторяющимися группами.
    DataDictionary – путь к словарю.
  • параметры клиента
    ReconnectInterval – интервал переподключения к серверу (в секундах).
    HeartBtInt – интервал проверочных сообщений типа HeartBeat (в секундах).
    LogonTimeout, LogoutTimeout – время ожидания Logon и Logout сообщений перед отключением сессии (в секундах).
    SocketConnectHost, SocketConnectPort – хост и порт подключения к acceptor-у.
  • параметры хранения сообщений и логов
    Сообщения и логи можно хранить в файлах или в базе данных (сообщения можно нигде не хранить, если выставить параметр PersistMessages=N).
    Я указала FileStorePath=target/data/banzai для хранения сообщений и номеров последовательностей в файле. Можно указать параметры базы данных (JdbcURL, JdbcUser, JdbcPassword и т.д), тогда сообщения будут храниться в базе данных.

В настройках конкретной сессии (в блоке [session]) главное – заполнить параметр BeginString, в котором указывается версия протокола FIX, использующегося в сообщениях.

Любые настройки можно указывать непосредственно при создании подключения в коде с помощью класса SessionSettings.

Подробнее о конфигурации клиента можно почитать в официальной документации.

Создание FIX-приложения

Теперь перейдем непосредственно к коду клиента. Чтобы создать FIX-приложение, нам нужно просто реализовать интерфейс Application:

public interface Application {
  void onCreate(SessionID sessionId);
  void onLogon(SessionID sessionId);
  void onLogout(SessionID sessionId);
  void toAdmin(Message message, SessionID sessionId);
  void toApp(Message message, SessionID sessionId)
    throws DoNotSend;
  void fromAdmin(Message message, SessionID sessionId)
    throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon;
  void fromApp(Message message, SessionID sessionId)
    throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType;
}

Эти методы вызываются в результате событий, происходящих в приложении (подробнее).

Метод fromApp срабатывает при получении сообщений с сервера, то есть в нем происходит основная логика. Остальные методы в основном служебные. Для удобства я создала абстрактный базовый класс BaseFixService, который реализует служебные методы интерфейса Application, и его наследника FixClientService, который занимается обработкой сообщений с сервера и соответственно реализует метод fromApp.

В приложении может быть установлено несколько сессий, поэтому в базовом классе будем хранить все сессии:

Map<SessionID, Session> sessions = new HashMap<>();

Так как метод onCreate срабатывает при создании новой сессии, в нем будем сохранять сессию по полученному ID с помощью метода lookupSession:

@Override
public void onCreate(SessionID sessionId) {
   log.info(">> onCreate for session: {}", sessionId);
   Session session = Session.lookupSession(sessionId);
   if (session != null) {
       sessions.put(sessionId, session);
   } else {
       log.warn("Requested session is not found.");
   }
}

Когда сессия отключается от сервера (мы завершили сеанс сообщением Logout или произошли какие-то технические проблемы и связь оборвалась), мы удаляем её из нашего хранилища.

@Override
public void onLogout(SessionID sessionId) {
   log.info(">> onLogout for session: {}", sessionId);
   sessions.remove(sessionId);
}

В FixClientService у нас находится главный обработчик сообщений – метод fromApp:

@Override
public void fromApp(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType {
   try {
       String type = MessageUtils.getMessageType(message.toString()); // получение типа сообщения
       switch (type) {
           case MARKET_DATA_SNAPSHOT_FULL_REFRESH:
               log.info("MarketData message: {}", message);
               break;
           case SECURITY_DEFINITION:
               log.info("SecurityDefinition message: {}", message);
               break;
           default:
               log.info("Unhandled message {} of type: {}", message, type);
       }
   } catch (Exception ex) {
       log.debug("Unexpected exception while processing message.", ex);
   }
}

С помощью класса MessageUtils библиотеки QuickFix/J можно получить тип входящего сообщения и далее обработать каждый случай (здесь для примера я указала несколько типов сообщений и вывела их в лог). В этой статье реализуем получение рыночных данных и их сохранение в кэш, остальные типы сообщений и их обработку более подробно разберем в следующих статьях и дополним логику нашего клиента.

Создание сервиса для подключения к серверу

Когда мы создали реализацию FIX-приложения, можно приступить к сервису для подключения к серверу – ConnectorService. При запуске приложения он будет создавать и запускать сокет для обмена сообщениями.

Для обмена сообщениями нужно создать SocketInitiator (на сервере аналогично создается SocketAcceptor). При создании передаются следующие параметры:

  • Application – FIX-приложение (т.е. класс, реализующий интерфейс Application, FixClientService в нашем случае)
  • MessageStoreFactory – способ хранения сообщений, это может быть, например, JdbcStoreFactory (хранение в базе данных), MemoryStoreFactory (хранение в памяти), FileStoreFactory (хранение в файле).
  • SessionSettings – настройки сессии, для их создания нужно передать файл с настройками (либо его название, либо InputStream).
  • LogFactory – хранение логов (аналогично сообщениям это может быть FileLogFactory, JdbcLogFactory), я использовала SLF4JLogFactory.
  • MessageFactory – используется для создания сообщений (можно использовать DefaultMessageFactory или MessageFactory для конкретной версии протокола FIX).

Путь к файлу настроек и дополнительные параметры (хост и порт подключения) для удобства я вынесла в конфигурацию приложения (application.yaml):

fix:
 cfg: 'classpath:config/client.cfg'
 socketConnectHost: localhost
 socketConnectPort: 9876

Соответственно при создании настроек сессии я использую этот файл и с помощью метода sessionSettings.set(String key, String value) добавляю параметры SocketConnectHost, SocketConnectPort:

try (InputStream inputStream = config.getCfg().getInputStream()) {
   SessionSettings sessionSettings = new SessionSettings(inputStream);
   sessionSettings.setString("SocketConnectHost", config.getSocketConnectHost());
   sessionSettings.setString("SocketConnectPort", config.getSocketConnectPort());

   MessageStoreFactory storeFactory = new FileStoreFactory(sessionSettings);
   SLF4JLogFactory logFactory = new SLF4JLogFactory(sessionSettings);
   MessageFactory messageFactory = new DefaultMessageFactory();

   socketInitiator = new SocketInitiator(fixClientService, storeFactory, sessionSettings, logFactory, messageFactory);
   socketInitiator.start();
} catch (Exception ex) {
   log.error("Exception while establishing connection to FIX server.", ex);
   throw new FixClientException("Exception while establishing connection to FIX server.", ex);
}

После создания настроек сессии объявляем LogFactory, MessageFactory, MessageStoreFactory и передаем их в конструктор SocketInitiator. Вызвав метод start() запустим подключение и сможем получать сообщения.

Не забудьте закрыть сокет при завершении работы с помощью метода stop().

Отправка запроса на получение рыночных данных

Когда приложение запустится и установится соединение с сервером, мы сможем отправлять и получать сообщения. Так как взаимодействие у нас построено на сокетах, отправка сообщения и получение ответа на него происходят асинхронно. Поэтому у нас будет два контроллера:

  • для инициации отправки сообщений
  • для получения данных, сохраненных в результате обработки ответов на отправленные ранее сообщения.

Чтобы получить рыночные данные (например, цену покупки, цену продажи инструмента), нам нужно отправить сообщение-запрос на данные и соответственно обработать ответное сообщение в методе fromApp.

Напишем метод для создания сообщения типа MarketDataRequest (о тегах сообщения можно почитать в спецификации).

public static Message createMarketDataRequest(String symbol) {
}

В библиотеке QuickFix/J все сообщения представляют собой классы, поля в которых соответствуют тегам. Можно создать экземпляр класса нужного нам сообщения и с помощью метода set() заполнить теги. Теги также представляют собой классы с обязательным полем FIELD, в котором хранится соответствующее числовое значение.

Например, тег symbol=55:

public class Symbol extends StringField {
   public static final int FIELD = 55;

   // constructors
}

Стандартные теги (соответствующие спецификации конкретной версии FIX) обычно можно заполнить напрямую. Например, для сообщения MarketDataRequest (далее буду сокращенно писать MDR) определены методы

set(SubscriptionRequestType value)
set(MarketDepth value)
set(Symbol value)
// ...

Если же при работе с конкретной биржей в сообщении присутствуют дополнительные теги, их можно задать с помощью общего метода setField(int key, Field<?> field): например, setField(5020, new IntField(10)) — добавим в сообщение тег <5020> со значением 10: 5020=10.

Создадим объект класса MarketDataRequest:

private static int mdReqID = 1;

MarketDataRequest marketDataRequest = new MarketDataRequest(
       new MDReqID(format("FixClient-%s", mdReqID++)),
       new SubscriptionRequestType(SNAPSHOT), //263
       new MarketDepth(1) //264, 1 = top of book
);

Подробнее о параметрах в конструкторе

В конструкторе передается три параметра:

  • MDReqID – уникальный в рамках данной сессии идентификатор сообщения.
  • SubscriptionRequestType:
    SNAPSHOT = ‘0’ (текущие данные);
    SNAPSHOT_PLUS_UPDATES = ‘1’ (текущие данные + подписка на обновление данных; при выборе этого типа каждый раз при изменении рыночных данных для инструмента, сервер будет отправлять сообщение типа MarketDataSnapshotFullRefresh с новыми данными);
    DISABLE_PREVIOUS_SNAPSHOT_PLUS_UPDATE_REQUEST = ‘2’ (отписка от получения данных + получение обновленных данных);
  • MarketDepth – глубина рынка для типа SNAPSHOT (цены формируются исходя из размещенных и ожидающих размещения заявок на покупку и продажу инструмента, эти заявки записываются в “книгу” заявок. Если указываем параметр равным 0 – будут учитываться все значения “книги”, если 1 – только “верхние” значения).

То же самое в виде сообщения: 262=FixClient-1 263=0 264=1.

Далее нужно указать параметры, которые мы хотим получить в результате запроса рыночных данных. Некоторые параметры в FIX-сообщениях задаются группами. При этом начинается такая часть сообщения с тега, в котором указывается количество последующих групп. В нашем случае параметр <267> NoMdEntryTypes хранит количество групп, а сами группы формируются из тегов <269> MdEntryType. Например, 269=0 означает, что мы хотим получить цену, по которой можно продать инструмент (Bid), а 269=1 – цену, по которой мы сможем купить инструмент (Ask, или Offer). Полный список стандартных значений этого тега можно посмотреть в спецификации. QuickFix/J автоматически заполняет в теге <267> количество параметров, мы можем только заполнить нужные нам поля и добавить каждую группу в сообщение:

   MarketDataRequest.NoMDEntryTypes group = new MarketDataRequest.NoMDEntryTypes(); //267

   group.set(new MDEntryType(MDEntryType.BID));
   marketDataRequest.addGroup(group);
   group.set(new MDEntryType(MDEntryType.OFFER));
   marketDataRequest.addGroup(group);

В сообщении будет выглядеть так: 267=2 269=0 269=1.

Можно делать MDR сразу для нескольких инструментов, в поле <146> NoRelatedSum передается их количество и далее заполняются группы тегов для каждого инструмента. Для простого запроса достаточно передать идентификатор инструмента в теге <55> (для более сложных запросов на фьючерсы или опционы нужно указывать дополнительные параметры, но для нашего базового случая это не нужно).

MarketDataRequest.NoRelatedSym instrument = new MarketDataRequest.NoRelatedSym();
instrument.set(new Symbol(symbol));
marketDataRequest.addGroup(instrument);

В сообщении: 146=1 55=AAPL.

Наш полученный MDR теперь можно отправить на сервер с помощью метода session.send():

@Override
public void sendMarkedDataRequest(String symbol) {
   sessions.forEach((sessionID, session) ->
           session.send(MsgUtils.createMarketDataRequest(symbol)));
}

Аналогично можно реализовать методы отправки любого другого сообщения (на создание заявки, на получение детальной информации об инструменте и т.д).

Для полученного метода отправки запроса на получение рыночных данных создадим REST endpoint, чтобы мы могли инициировать его отправку:

@PostMapping(value = "/market-data-request")
public void sendMarketDataRequest(@RequestParam("symbol") String symbol) {
   fixClientService.sendMarkedDataRequest(symbol);
}

Так будет выглядеть запрос, чтобы создать и отправить сообщение для получения данных об акциях Apple:
POST localhost:9090/fix-client/v1/market-data-request?symbol=APPL.

В результате будет отправлено сообщение:

8=FIX.4.2 9=117 35=V 34=3 49=FIX_CLIENT 52=20200601-17:10:34.103 56=EXEC 262=FixClient-1 263=0 264=1 146=1 55=AAPL 267=2 269=0 269=1 10=018

Обработка ответа и сохранение рыночных данных

Создадим отдельный сервис (MarketDataService), который будет обрабатывать рыночные данные, полученные в результате отправки запроса. Он будет сохранять полученные данные в объект, записывать их в память и отдавать при запросе по идентификатору инструмента.

Класс для хранения рыночных данных:

@Data
@Accessors(chain = true)
public class MarketDataModel {
   private String symbol;
   private BigDecimal bid;
   private BigDecimal ask;
}

Теперь нужно разобраться, как правильно обработать сообщение с данными и сохранить его.
Вот так выглядит сообщение, отправленное нам в ответ на запрос по символу AAPL:

8=FIX.4.2 9=104 35=W 34=3 49=EXEC 52=20200601-17:10:34.119 56=FIX_CLIENT 55=AAPL 262=FixClient-1 268=1 269=0 270=123.45 10=236.

Так как при запросе мы указывали группы параметров (Bid, Ask и т.д), разбирать сообщение тоже будем по группам:

message.getGroups(NoMDEntries.FIELD).forEach(group -> {
   int type = MsgUtils.getIntField(group, MDEntryType.FIELD).orElse(-1);
   BigDecimal value = MsgUtils.getDecimalField(group, MDEntryPx.FIELD).orElse(BigDecimal.ZERO);

   switch (type) {
       case 0:
           dataModel.setBid(value);
           break;
       case 1:
           dataModel.setAsk(value);
           break;
       default:
           log.warn("Invalid entry type: {}", type);
           break;
   }
});

В теге <269> хранится название параметра, а в теге <270> его значение. Соответственно, если тип параметра = 0 (т.е. Bid), то мы сохраняем значение соответствующего ему тега <270> в поле bid нашего объекта.

Далее проверяем тег <55> – идентификатор инструмента, и сохраняем по нему наши данные:

MsgUtils.getStrField(message, Symbol.FIELD).ifPresent(s -> {
   dataModel.setSymbol(s);
   marketData.put(s, dataModel);
});

Осталось только добавить сохранение данных в метод fromApp в случай обработки сообщения типа MarketDataSnapshotFullRefresh:

case MARKET_DATA_SNAPSHOT_FULL_REFRESH:
   marketDataService.saveMarketData(message);
   break;

Теперь при получении нашим приложением сообщения типа MarketDataSnapshotFullRefresh будет происходить обработка и сохранение данных в память приложения.

Соответственно в отдельный Rest-Controller добавляем метод получения данных по идентификатору:

@GetMapping
public ResponseEntity<MarketDataModel> getMarketData(@RequestParam("symbol") String symbol) {
   return new ResponseEntity<>(marketDataService.getMarketData(symbol), HttpStatus.OK);
}

Вызвав метод GET localhost:9090/fix-client/v1/market-data?symbol=AAPL
получим ответ:

{
  "symbol": "AAPL",
  "bid": 123.45,
  "ask": null
}

Запуск приложения

Наконец, можем запустить наше приложение, убедиться, что подключение к серверу осуществляется успешно, и попробовать отправить запрос на получение рыночных данных.

Если при запуске приложения в логах отображаются ошибки подключения (ConnectException), как на скриншоте ниже, проверьте, что сервер запущен и что вы указали правильные идентификаторы клиента и сервера и хост и порт для подключения:

В случае успешного запуска клиент и сервер должны обменяться Logon-сообщениями:

Отправим запрос POST localhost:9090/fix-client/v1/market-data-request?symbol=APPL, чтобы вызвать отправку сообщения MDR и убедимся, что сообщение действительно отправлено и ответ на него получен:

Бонус

Кстати, сообщения можно удобно парсить с помощью сайта – просто вставляете текст сообщения и получаете разбор по тегам и значениям:

Теперь вызвав метод GET localhost:9090/fix-client/v1/market-data?symbol=AAPL
мы должны получить ответ:

{
  "symbol": "AAPL",
  "bid": 123.45,
  "ask": null
}

Работает!

Конечно, на таком “игрушечном” примере далеко не уедешь, но для начала он хорошо подходит. Для более сложных примеров и для работы с условиями, приближенными к реальной бирже, можно получить доступ к тестовому контуру Московской биржи (MOEX) — для этого нужно оставить заявку на сайте. Я не нашла аналогичных тестовых контуров у других крупных бирж (именно для подключения напрямую через FIX-протокол), кроме симуляторов биржевой торговли, где выдаются виртуальные деньги и с помощью терминалов осуществляется торговля. Если знаете, где найти хороший тестовый сервер для работы по протоколу FIX, — поделитесь в комментариях, буду благодарна.

В следующей статье я планирую рассмотреть основные виды FIX-сообщений (соответственно дополнить приложение методами для их создания) и далее перейти к подробному рассмотрению процесса создания торговых заявок и их обработки биржей. Все примеры сообщений по-прежнему можно создавать с помощью приложения MiniFIX, если не хотите писать реализацию своего клиента.

Java, Софт, Финансы в IT


Рекомендация: подборка платных и бесплатных курсов Java — https://katalog-kursov.ru/


В предыдущей статье мы использовали приложение MiniFIX для подключения и отправки сообщений на тестовую биржу с помощью протокола FIX. В этой статье напишем собственную реализацию клиента для получения рыночных данных в виде небольшого SpringBoot-приложения. Код доступен в репозитории.

Для реализации приложения нам понадобится:

  • Java 8
  • Maven
  • Spring boot 2.2.5
  • Lombok
  • QuickFix/J

Содержание для упрощения навигации по статье:

  • FIX-Engine и запуск тестового сервера
  • Структура проекта
  • Настройка параметров подключения
  • Создание FIX-приложения
  • Создание сервиса для подключения к серверу
  • Отправка запроса на получение рыночных данных
  • Обработка ответа и сохранение рыночных данных
  • Запуск приложения

FIX-Engine и запуск тестового сервера

FIX-Engine, или FIX-движок, обеспечивает связь со сторонними системами по протоколу FIX. Он отвечает за преобразование данных в FIX-сообщения, а также за создание сессии и обеспечение ее работы: проверку валидности сообщений, генерацию контрольных сумм, восстановление работы после потери связи и т.д (здесь можно почитать более подробно).

В нашем случае в роли такого движка выступает QuickFix/J. В предыдущей части я использовала пример Executor из модуля examples, но в нем обрабатываются только сообщения на создание торговых заявок. В этом же модуле есть более подходящий пример — OrderMatch (quickfixj-examples-ordermatch), в нем помимо поддержки торговых заявок присутствует обработка сообщений на получение рыночных данных (MarketDataRequest).

Когда вы первый раз клонируете репозиторий, обязательно нужно выполнить сборку проекта, чтобы сгенерировались FIX-сообщения для различных версий протокола. В Readme проекта есть описание команд для различных видов сборки (с тестами и без), самый быстрый:

mvn clean package -Dmaven.javadoc.skip=true -DskipTests -PskipBundlePlugin

Процесс сборки длился у меня где-то минут 6-7, так что в это время можно заварить себе чашечку чая изучить настройки сервера и приступить к написанию клиента.

Как я уже описывала ранее, открываем файл resources/quickfix.examples.ordermatch/ordermatch.cfg, проверяем SocketAcceptPort и заполняем поле TargetCompID нужным значением для нашего клиента (можно оставить BANZAI, которое указано по умолчанию, можно написать любое другое на ваше усмотрение):

[default]
FileStorePath=target/data/ordermatch
DataDictionary=FIX42.xml
SocketAcceptPort=9876 // порт для подключения
BeginString=FIX.4.2 // версия FIX 4.2

[session]
SenderCompID=EXEC // идентификатор сервера
TargetCompID=FIX_CLIENT // идентификатор клиента
ConnectionType=acceptor
StartTime=00:00:00
EndTime=00:00:00

Если хотите поменять значение идентификатора клиента, то лучше, конечно, сделать это перед сборкой, чтобы не пришлось собирать еще раз.

Когда сборка завершится, заходим в quickfixjquickfixj-examplesordermatchtarget, проверяем, что там появились *.jar файлы:

Запускаем файл quickfixj-examples-ordermatch-2.2.0-SNAPSHOT-standalone.jar, так как он содержит все необходимые для запуска зависимости:

java -jar quickfixj-examples-ordermatch-2.2.0-SNAPSHOT-standalone.jar

Если появилась запись «Started QFJ Message Processor» – значит, сервер запустился. Проверьте, что в строке «Listening for connections at … [FIX4.2:EXEC->FIX_CLIENT]» указано нужное значение идентификатора клиента.

Структура проекта

Вот так выглядит готовый проект (стандартная структура веб-приложений: сервисы, контроллеры, модельки и т.д):

Создаем maven-проект со стандартными зависимостями и добавляем библиотеку QuickFix/J для работы с протоколом FIX:

<properties>
   <quickfixj.version>2.0.0</quickfixj.version>
</properties>
<dependency>
   <groupId>org.quickfixj</groupId>
   <artifactId>quickfixj-core</artifactId>
   <version>${quickfixj.version}</version>
</dependency>

<dependency>
   <groupId>org.quickfixj</groupId>
   <artifactId>quickfixj-messages-fix42</artifactId>
   <version>${quickfixj.version}</version>
</dependency>

Я подключила 2 модуля: quickfixj-core и quickfixj-messages-fix42 для работы с сообщениями только версии FIX 4.2.

Если в вашем приложении предполагаются сообщения различных версий протокола, можно подключить quickfixj-core + quickfixj-messages-all или просто quickfixj-all.

Полная версия pom.xml доступна в репозитории.

Настройка параметров подключения

По аналогии с файлом настроек на сервере, создадим файл resources/config/client.cfg с настройками нашего приложения.

В файле может быть один блок [default], в котором находятся параметры, общие для всех сессий, и несколько блоков [session] для описания параметров конкретной сессии (если сервер поддерживает сообщения различных версий протокола FIX, то для каждой версии создается отдельный блок [session]).

[default]
SenderCompID=FIX_CLIENT // идентификатор отправителя
TargetCompID=EXEC // идентификатор получателя
ConnectionType=initiator // приложение является клиентом
NonStopSession=Y
SocketConnectHost=localhost
ReconnectInterval=5
HeartBtInt=30
FileStorePath=target/data/banzai
UseDataDictionary=Y
DataDictionary=dictionary/fix4_2.xml
ValidateUserDefinedFields=N
AllowUnknownMsgFields=Y
ValidateUserDefinedFields=N
AllowUnknownMsgFields=Y

[session]
BeginString=FIX.4.2
ResetOnLogon=Y

Подробнее о параметрах

Начнем с блока [default]:

  • параметры сессии
    SenderCompID, TargetCompID – идентификатор отправителя и получателя сообщений соответственно (sender – наше приложение, target – сервер). Убедитесь, что эти значения совпадают со значениями параметров на сервере.
    ConnectionType (initiator/acceptor) – указывает, является наше приложение клиентом или сервером.
    — С помощью параметров StartTime и EndTime можно указать время начала и соответственно завершения работы сессии (например, биржа работает с 9.00 до 18.00, поэтому нет смысла запускать сессию вне этого времени). Если же сессия будет работать весь день, то можно указать NonStopSession=Y, что будет равносильно варианту StartTime=00:00:00 и EndTime=00:00:00.
    -параметры валидации сообщений
    UseDataDictionary=Y – можно использовать словарь сообщений, если вы работаете с биржей, спецификация сообщений которой отличается от стандартной (например, в словаре можно указать дополнительные теги или типы сообщений). При этом использование словаря обязательно, если есть сообщения с повторяющимися группами.
    DataDictionary – путь к словарю.
  • параметры клиента
    ReconnectInterval – интервал переподключения к серверу (в секундах).
    HeartBtInt – интервал проверочных сообщений типа HeartBeat (в секундах).
    LogonTimeout, LogoutTimeout – время ожидания Logon и Logout сообщений перед отключением сессии (в секундах).
    SocketConnectHost, SocketConnectPort – хост и порт подключения к acceptor-у.
  • параметры хранения сообщений и логов
    Сообщения и логи можно хранить в файлах или в базе данных (сообщения можно нигде не хранить, если выставить параметр PersistMessages=N).
    Я указала FileStorePath=target/data/banzai для хранения сообщений и номеров последовательностей в файле. Можно указать параметры базы данных (JdbcURL, JdbcUser, JdbcPassword и т.д), тогда сообщения будут храниться в базе данных.

В настройках конкретной сессии (в блоке [session]) главное – заполнить параметр BeginString, в котором указывается версия протокола FIX, использующегося в сообщениях.

Любые настройки можно указывать непосредственно при создании подключения в коде с помощью класса SessionSettings.

Подробнее о конфигурации клиента можно почитать в официальной документации.

Создание FIX-приложения

Теперь перейдем непосредственно к коду клиента. Чтобы создать FIX-приложение, нам нужно просто реализовать интерфейс Application:

public interface Application {
  void onCreate(SessionID sessionId);
  void onLogon(SessionID sessionId);
  void onLogout(SessionID sessionId);
  void toAdmin(Message message, SessionID sessionId);
  void toApp(Message message, SessionID sessionId)
    throws DoNotSend;
  void fromAdmin(Message message, SessionID sessionId)
    throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon;
  void fromApp(Message message, SessionID sessionId)
    throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType;
}

Эти методы вызываются в результате событий, происходящих в приложении (подробнее).

Метод fromApp срабатывает при получении сообщений с сервера, то есть в нем происходит основная логика. Остальные методы в основном служебные. Для удобства я создала абстрактный базовый класс BaseFixService, который реализует служебные методы интерфейса Application, и его наследника FixClientService, который занимается обработкой сообщений с сервера и соответственно реализует метод fromApp.

В приложении может быть установлено несколько сессий, поэтому в базовом классе будем хранить все сессии:

Map<SessionID, Session> sessions = new HashMap<>();

Так как метод onCreate срабатывает при создании новой сессии, в нем будем сохранять сессию по полученному ID с помощью метода lookupSession:

@Override
public void onCreate(SessionID sessionId) {
   log.info(">> onCreate for session: {}", sessionId);
   Session session = Session.lookupSession(sessionId);
   if (session != null) {
       sessions.put(sessionId, session);
   } else {
       log.warn("Requested session is not found.");
   }
}

Когда сессия отключается от сервера (мы завершили сеанс сообщением Logout или произошли какие-то технические проблемы и связь оборвалась), мы удаляем её из нашего хранилища.

@Override
public void onLogout(SessionID sessionId) {
   log.info(">> onLogout for session: {}", sessionId);
   sessions.remove(sessionId);
}

В FixClientService у нас находится главный обработчик сообщений – метод fromApp:

@Override
public void fromApp(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType {
   try {
       String type = MessageUtils.getMessageType(message.toString()); // получение типа сообщения
       switch (type) {
           case MARKET_DATA_SNAPSHOT_FULL_REFRESH:
               log.info("MarketData message: {}", message);
               break;
           case SECURITY_DEFINITION:
               log.info("SecurityDefinition message: {}", message);
               break;
           default:
               log.info("Unhandled message {} of type: {}", message, type);
       }
   } catch (Exception ex) {
       log.debug("Unexpected exception while processing message.", ex);
   }
}

С помощью класса MessageUtils библиотеки QuickFix/J можно получить тип входящего сообщения и далее обработать каждый случай (здесь для примера я указала несколько типов сообщений и вывела их в лог). В этой статье реализуем получение рыночных данных и их сохранение в кэш, остальные типы сообщений и их обработку более подробно разберем в следующих статьях и дополним логику нашего клиента.

Создание сервиса для подключения к серверу

Когда мы создали реализацию FIX-приложения, можно приступить к сервису для подключения к серверу – ConnectorService. При запуске приложения он будет создавать и запускать сокет для обмена сообщениями.

Для обмена сообщениями нужно создать SocketInitiator (на сервере аналогично создается SocketAcceptor). При создании передаются следующие параметры:

  • Application – FIX-приложение (т.е. класс, реализующий интерфейс Application, FixClientService в нашем случае)
  • MessageStoreFactory – способ хранения сообщений, это может быть, например, JdbcStoreFactory (хранение в базе данных), MemoryStoreFactory (хранение в памяти), FileStoreFactory (хранение в файле).
  • SessionSettings – настройки сессии, для их создания нужно передать файл с настройками (либо его название, либо InputStream).
  • LogFactory – хранение логов (аналогично сообщениям это может быть FileLogFactory, JdbcLogFactory), я использовала SLF4JLogFactory.
  • MessageFactory – используется для создания сообщений (можно использовать DefaultMessageFactory или MessageFactory для конкретной версии протокола FIX).

Путь к файлу настроек и дополнительные параметры (хост и порт подключения) для удобства я вынесла в конфигурацию приложения (application.yaml):

fix:
 cfg: 'classpath:config/client.cfg'
 socketConnectHost: localhost
 socketConnectPort: 9876

Соответственно при создании настроек сессии я использую этот файл и с помощью метода sessionSettings.set(String key, String value) добавляю параметры SocketConnectHost, SocketConnectPort:

try (InputStream inputStream = config.getCfg().getInputStream()) {
   SessionSettings sessionSettings = new SessionSettings(inputStream);
   sessionSettings.setString("SocketConnectHost", config.getSocketConnectHost());
   sessionSettings.setString("SocketConnectPort", config.getSocketConnectPort());

   MessageStoreFactory storeFactory = new FileStoreFactory(sessionSettings);
   SLF4JLogFactory logFactory = new SLF4JLogFactory(sessionSettings);
   MessageFactory messageFactory = new DefaultMessageFactory();

   socketInitiator = new SocketInitiator(fixClientService, storeFactory, sessionSettings, logFactory, messageFactory);
   socketInitiator.start();
} catch (Exception ex) {
   log.error("Exception while establishing connection to FIX server.", ex);
   throw new FixClientException("Exception while establishing connection to FIX server.", ex);
}

После создания настроек сессии объявляем LogFactory, MessageFactory, MessageStoreFactory и передаем их в конструктор SocketInitiator. Вызвав метод start() запустим подключение и сможем получать сообщения.

Не забудьте закрыть сокет при завершении работы с помощью метода stop().

Отправка запроса на получение рыночных данных

Когда приложение запустится и установится соединение с сервером, мы сможем отправлять и получать сообщения. Так как взаимодействие у нас построено на сокетах, отправка сообщения и получение ответа на него происходят асинхронно. Поэтому у нас будет два контроллера:

  • для инициации отправки сообщений
  • для получения данных, сохраненных в результате обработки ответов на отправленные ранее сообщения.

Чтобы получить рыночные данные (например, цену покупки, цену продажи инструмента), нам нужно отправить сообщение-запрос на данные и соответственно обработать ответное сообщение в методе fromApp.

Напишем метод для создания сообщения типа MarketDataRequest (о тегах сообщения можно почитать в спецификации).

public static Message createMarketDataRequest(String symbol) {
}

В библиотеке QuickFix/J все сообщения представляют собой классы, поля в которых соответствуют тегам. Можно создать экземпляр класса нужного нам сообщения и с помощью метода set() заполнить теги. Теги также представляют собой классы с обязательным полем FIELD, в котором хранится соответствующее числовое значение.

Например, тег symbol=55:

public class Symbol extends StringField {
   public static final int FIELD = 55;

   // constructors
}

Стандартные теги (соответствующие спецификации конкретной версии FIX) обычно можно заполнить напрямую. Например, для сообщения MarketDataRequest (далее буду сокращенно писать MDR) определены методы

set(SubscriptionRequestType value)
set(MarketDepth value)
set(Symbol value)
// ...

Если же при работе с конкретной биржей в сообщении присутствуют дополнительные теги, их можно задать с помощью общего метода setField(int key, Field<?> field): например, setField(5020, new IntField(10)) — добавим в сообщение тег <5020> со значением 10: 5020=10.

Создадим объект класса MarketDataRequest:

private static int mdReqID = 1;

MarketDataRequest marketDataRequest = new MarketDataRequest(
       new MDReqID(format("FixClient-%s", mdReqID++)),
       new SubscriptionRequestType(SNAPSHOT), //263
       new MarketDepth(1) //264, 1 = top of book
);

Подробнее о параметрах в конструкторе

В конструкторе передается три параметра:

  • MDReqID – уникальный в рамках данной сессии идентификатор сообщения.
  • SubscriptionRequestType:
    SNAPSHOT = ‘0’ (текущие данные);
    SNAPSHOT_PLUS_UPDATES = ‘1’ (текущие данные + подписка на обновление данных; при выборе этого типа каждый раз при изменении рыночных данных для инструмента, сервер будет отправлять сообщение типа MarketDataSnapshotFullRefresh с новыми данными);
    DISABLE_PREVIOUS_SNAPSHOT_PLUS_UPDATE_REQUEST = ‘2’ (отписка от получения данных + получение обновленных данных);
  • MarketDepth – глубина рынка для типа SNAPSHOT (цены формируются исходя из размещенных и ожидающих размещения заявок на покупку и продажу инструмента, эти заявки записываются в “книгу” заявок. Если указываем параметр равным 0 – будут учитываться все значения “книги”, если 1 – только “верхние” значения).

То же самое в виде сообщения: 262=FixClient-1 263=0 264=1.

Далее нужно указать параметры, которые мы хотим получить в результате запроса рыночных данных. Некоторые параметры в FIX-сообщениях задаются группами. При этом начинается такая часть сообщения с тега, в котором указывается количество последующих групп. В нашем случае параметр <267> NoMdEntryTypes хранит количество групп, а сами группы формируются из тегов <269> MdEntryType. Например, 269=0 означает, что мы хотим получить цену, по которой можно продать инструмент (Bid), а 269=1 – цену, по которой мы сможем купить инструмент (Ask, или Offer). Полный список стандартных значений этого тега можно посмотреть в спецификации. QuickFix/J автоматически заполняет в теге <267> количество параметров, мы можем только заполнить нужные нам поля и добавить каждую группу в сообщение:

   MarketDataRequest.NoMDEntryTypes group = new MarketDataRequest.NoMDEntryTypes(); //267

   group.set(new MDEntryType(MDEntryType.BID));
   marketDataRequest.addGroup(group);
   group.set(new MDEntryType(MDEntryType.OFFER));
   marketDataRequest.addGroup(group);

В сообщении будет выглядеть так: 267=2 269=0 269=1.

Можно делать MDR сразу для нескольких инструментов, в поле <146> NoRelatedSum передается их количество и далее заполняются группы тегов для каждого инструмента. Для простого запроса достаточно передать идентификатор инструмента в теге <55> (для более сложных запросов на фьючерсы или опционы нужно указывать дополнительные параметры, но для нашего базового случая это не нужно).

MarketDataRequest.NoRelatedSym instrument = new MarketDataRequest.NoRelatedSym();
instrument.set(new Symbol(symbol));
marketDataRequest.addGroup(instrument);

В сообщении: 146=1 55=AAPL.

Наш полученный MDR теперь можно отправить на сервер с помощью метода session.send():

@Override
public void sendMarkedDataRequest(String symbol) {
   sessions.forEach((sessionID, session) ->
           session.send(MsgUtils.createMarketDataRequest(symbol)));
}

Аналогично можно реализовать методы отправки любого другого сообщения (на создание заявки, на получение детальной информации об инструменте и т.д).

Для полученного метода отправки запроса на получение рыночных данных создадим REST endpoint, чтобы мы могли инициировать его отправку:

@PostMapping(value = "/market-data-request")
public void sendMarketDataRequest(@RequestParam("symbol") String symbol) {
   fixClientService.sendMarkedDataRequest(symbol);
}

Так будет выглядеть запрос, чтобы создать и отправить сообщение для получения данных об акциях Apple:
POST localhost:9090/fix-client/v1/market-data-request?symbol=APPL.

В результате будет отправлено сообщение:

8=FIX.4.2 9=117 35=V 34=3 49=FIX_CLIENT 52=20200601-17:10:34.103 56=EXEC 262=FixClient-1 263=0 264=1 146=1 55=AAPL 267=2 269=0 269=1 10=018

Обработка ответа и сохранение рыночных данных

Создадим отдельный сервис (MarketDataService), который будет обрабатывать рыночные данные, полученные в результате отправки запроса. Он будет сохранять полученные данные в объект, записывать их в память и отдавать при запросе по идентификатору инструмента.

Класс для хранения рыночных данных:

@Data
@Accessors(chain = true)
public class MarketDataModel {
   private String symbol;
   private BigDecimal bid;
   private BigDecimal ask;
}

Теперь нужно разобраться, как правильно обработать сообщение с данными и сохранить его.
Вот так выглядит сообщение, отправленное нам в ответ на запрос по символу AAPL:

8=FIX.4.2 9=104 35=W 34=3 49=EXEC 52=20200601-17:10:34.119 56=FIX_CLIENT 55=AAPL 262=FixClient-1 268=1 269=0 270=123.45 10=236.

Так как при запросе мы указывали группы параметров (Bid, Ask и т.д), разбирать сообщение тоже будем по группам:

message.getGroups(NoMDEntries.FIELD).forEach(group -> {
   int type = MsgUtils.getIntField(group, MDEntryType.FIELD).orElse(-1);
   BigDecimal value = MsgUtils.getDecimalField(group, MDEntryPx.FIELD).orElse(BigDecimal.ZERO);

   switch (type) {
       case 0:
           dataModel.setBid(value);
           break;
       case 1:
           dataModel.setAsk(value);
           break;
       default:
           log.warn("Invalid entry type: {}", type);
           break;
   }
});

В теге <269> хранится название параметра, а в теге <270> его значение. Соответственно, если тип параметра = 0 (т.е. Bid), то мы сохраняем значение соответствующего ему тега <270> в поле bid нашего объекта.

Далее проверяем тег <55> – идентификатор инструмента, и сохраняем по нему наши данные:

MsgUtils.getStrField(message, Symbol.FIELD).ifPresent(s -> {
   dataModel.setSymbol(s);
   marketData.put(s, dataModel);
});

Осталось только добавить сохранение данных в метод fromApp в случай обработки сообщения типа MarketDataSnapshotFullRefresh:

case MARKET_DATA_SNAPSHOT_FULL_REFRESH:
   marketDataService.saveMarketData(message);
   break;

Теперь при получении нашим приложением сообщения типа MarketDataSnapshotFullRefresh будет происходить обработка и сохранение данных в память приложения.

Соответственно в отдельный Rest-Controller добавляем метод получения данных по идентификатору:

@GetMapping
public ResponseEntity<MarketDataModel> getMarketData(@RequestParam("symbol") String symbol) {
   return new ResponseEntity<>(marketDataService.getMarketData(symbol), HttpStatus.OK);
}

Вызвав метод GET localhost:9090/fix-client/v1/market-data?symbol=AAPL
получим ответ:

{
  "symbol": "AAPL",
  "bid": 123.45,
  "ask": null
}

Запуск приложения

Наконец, можем запустить наше приложение, убедиться, что подключение к серверу осуществляется успешно, и попробовать отправить запрос на получение рыночных данных.

Если при запуске приложения в логах отображаются ошибки подключения (ConnectException), как на скриншоте ниже, проверьте, что сервер запущен и что вы указали правильные идентификаторы клиента и сервера и хост и порт для подключения:

В случае успешного запуска клиент и сервер должны обменяться Logon-сообщениями:

Отправим запрос POST localhost:9090/fix-client/v1/market-data-request?symbol=APPL, чтобы вызвать отправку сообщения MDR и убедимся, что сообщение действительно отправлено и ответ на него получен:

Бонус

Кстати, сообщения можно удобно парсить с помощью сайта – просто вставляете текст сообщения и получаете разбор по тегам и значениям:

Теперь вызвав метод GET localhost:9090/fix-client/v1/market-data?symbol=AAPL
мы должны получить ответ:

{
  "symbol": "AAPL",
  "bid": 123.45,
  "ask": null
}

Работает!

Конечно, на таком “игрушечном” примере далеко не уедешь, но для начала он хорошо подходит. Для более сложных примеров и для работы с условиями, приближенными к реальной бирже, можно получить доступ к тестовому контуру Московской биржи (MOEX) — для этого нужно оставить заявку на сайте. Я не нашла аналогичных тестовых контуров у других крупных бирж (именно для подключения напрямую через FIX-протокол), кроме симуляторов биржевой торговли, где выдаются виртуальные деньги и с помощью терминалов осуществляется торговля. Если знаете, где найти хороший тестовый сервер для работы по протоколу FIX, — поделитесь в комментариях, буду благодарна.

В следующей статье я планирую рассмотреть основные виды FIX-сообщений (соответственно дополнить приложение методами для их создания) и далее перейти к подробному рассмотрению процесса создания торговых заявок и их обработки биржей. Все примеры сообщений по-прежнему можно создавать с помощью приложения MiniFIX, если не хотите писать реализацию своего клиента.

SnowRunner — это безбашенный симулятор гонок по обычным российским дорогам лесным проселкам, горным утесам и прочим, не приспособленным для экстремального вождения, местам. Лес, снег, грязь — что еще нужно для веселого времяпрепровождения? Ах да, возможность посоревноваться с друзьями в кооперативе.

Snowrunner

Но эти виртуальные гоночки стоят 1199 рублей в Steam и Epic Games, на минуточку. А кооператив на пиратке — как единорог. Его видели только сумасшедшие. Ну, а мы все-таки расскажем, как запустить SnowRunner по сети и поиграть с друзьями.

Данная статья носит исключительно образовательный характер. Мы не одобряем пиратство и не призываем к нему!

Варианты подключения к кооперативу

Snowrunner

SnowRunner — это уникальный гоночный симулятор. В качестве препятствий в гонке — погодные условия. Чтобы справиться с ними, придется обновлять машину и модернизировать ее. Для облегчения проезда мы можем использовать грунтозацепы в виде цепей и прочих аксессуаров. Помимо обычных заездов у нас будет большое количество заданий по типу «Тачки на прокачку». 

Очевидно, что у пиратских игр нет прямого доступа к серверам игры, в отличие от оригинальных игр. Это и мотивирует умельцев из народа пошевелить мозгами и создать подходящие варианты: 

  • неофициальное подключение к игровым серверам с пиратской версией игры;
  • взломанный клиент игры.

Давайте разберемся, какой из этих двух вариантов подходит в нашем случае и запустит мультиплеер на пиратской версии игры.

Загружаем кооператив на пиратку

MudRunner

В игре существует два режима:

  • одиночный;
  • кооперативный.

В кооперативном режиме вы сражаетесь в гонке с другими игроками. Максимальное количество возможных игроков в кооперативном режиме — три. Для запуска кооператива игру нужно правильно пропатчить. Инструкцию для установки патча смотрите далее. 

Устанавливаем патч

Snowrunner

Как только вы установили пиратскую версию игры, переходим на сайт и пролистываем вниз. Там нас должна ждать кнопочка с подписью «fix online-coop». 

Snowrunner

Она ведет вас на облако mail.ru, где лежит много-много дополнительных архивов. Это карты для проведения кооперативных миссий. Поэтому просто нажимаем кнопку «Скачать» в верхнем меню и ждем завершения скачивания.

Snowrunner

В списке ниже мы видим, что для работы онлайна соединение нужно подключать через LAN и интернет должен работать, что очевидно. С самого начала нам напоминают, что надо бы установить саму игру. 

Как только пиратка SnowRunner поставлена на ваш ПК, скачивайте патч. Следуйте всем этапам установки и, вуаля, вы почти готовы садиться в онлайн-грузовик и рассекать по бездорожью.

MudRunner

Начинаем наше приключение с запуска самой игры. Это можно сделать через ярлык на рабочем столе или файл «Game.exe», который лежит в корневой папке игры.

Запуск игры по сети производится с помощью специальных программ: Tunngle, Evolve, Radmin VPN. Это сервисы для подключения к серверам через VPN. Если вы не доверяете этому софту, можете подыскать подобное ПО, которому доверяете больше. Наш гайд носит лишь рекомендательный характер. 

Snowrunner

Если ваш друг уже создал гонку и единственный, кого он ждет, это вы, то:

  • в меню выбирайте пункт «Matchmaking»;
  • среди предложенных комнат выбирайте игру, созданную вашим другом;
  • наслаждайтесь кооперативным режимом.

А если вы сами хотите создать сервер для друзей, то в этом случае: 

  • выбираем кнопку «Host game»;
  • ждем в комнате друзей для игры;
  • катаем серьезные гонки по адским трассам.

Таким образом, мы можем поиграть с друзьями в SnowRunner, даже если располагаем только пиратским аккаунтом! Готовьте вашу лучшую резину, эта гонка будет запарной!

Взламываем саму игру

MudRunner

Если по сети у вас поиграть не получилось, то предлагаем создать взломанную версию самостоятельно. Пиратка у вас есть, нам нужно найти «кряк», который откроет нам возможности кооперативной игры. 

Snowrunner

Разархивируйте все файлы из файла RAR и переместите их в корневую папку, куда вы устанавливали вашу игру.

Если вы запамятовали, где лежит игра, то нажимаем ПКМ по ярлыку игры на рабочем столе и в выпавшем меню ищем пункт «Расположение файла». Сразу после этого вам откроется папка со всеми файлами для SnowRunner.

В эту папку скопируйте содержимое архива, который скачали ранее. Когда система спросит, заменить ли файлы в корневой папке игры, вы нажимаете галочку на пункте «для всех файлов» и система заменит все исходные файлы на поломанные. 

Snowrunner

Теперь попробуйте вновь запустить игру и активировать многопользовательский режим. В этот раз должно получиться. Этот способ не требует дополнительного софта, такого как Hamachi и прочее. После запуска следуйте всем инструкциям, которые мы дали в конце предыдущего пункта. 

Если игра вам так понравилась, что вы решили ее купить, предлагаем воспользоваться распродажами в Epic Store. В среднем, можно сэкономить на покупке игры 650 рублей. С одной стороны, придется вложить какую-то копеечку в игру. Зато никаких костылей для мультиплеера придумывать не придется.

Готовьтесь застревать в грязи, тонуть в снегу и жечь мотор в приступе буксирования. Если хотите попробовать гоночные симуляторы в других антуражах, то обратите внимание на другие наши статьи!

Другие статьи по теме:

  • Лучшие гонки для слабых ПК с хорошей графикой
  • 8 вещей, которые бесят игроков в гонках
  • 10 лучших гоночных игр прошедшего десятилетия
  • 20 лучших и самых крутых гоночных игр на ПК
  • Топ-10 взрывных игр с лучшим разрушаемым окружением

Заглавное фото: wallpaperflare.com

Статьи по теме

Тэги

Время на прочтение
15 мин

Количество просмотров 9.5K


В предыдущей статье мы использовали приложение MiniFIX для подключения и отправки сообщений на тестовую биржу с помощью протокола FIX. В этой статье напишем собственную реализацию клиента для получения рыночных данных в виде небольшого SpringBoot-приложения. Код доступен в репозитории.

Для реализации приложения нам понадобится:

  • Java 8
  • Maven
  • Spring boot 2.2.5
  • Lombok
  • QuickFix/J

Содержание для упрощения навигации по статье:

  • FIX-Engine и запуск тестового сервера
  • Структура проекта
  • Настройка параметров подключения
  • Создание FIX-приложения
  • Создание сервиса для подключения к серверу
  • Отправка запроса на получение рыночных данных
  • Обработка ответа и сохранение рыночных данных
  • Запуск приложения

FIX-Engine и запуск тестового сервера

FIX-Engine, или FIX-движок, обеспечивает связь со сторонними системами по протоколу FIX. Он отвечает за преобразование данных в FIX-сообщения, а также за создание сессии и обеспечение ее работы: проверку валидности сообщений, генерацию контрольных сумм, восстановление работы после потери связи и т.д (здесь можно почитать более подробно).

В нашем случае в роли такого движка выступает QuickFix/J. В предыдущей части я использовала пример Executor из модуля examples, но в нем обрабатываются только сообщения на создание торговых заявок. В этом же модуле есть более подходящий пример — OrderMatch (quickfixj-examples-ordermatch), в нем помимо поддержки торговых заявок присутствует обработка сообщений на получение рыночных данных (MarketDataRequest).

Когда вы первый раз клонируете репозиторий, обязательно нужно выполнить сборку проекта, чтобы сгенерировались FIX-сообщения для различных версий протокола. В Readme проекта есть описание команд для различных видов сборки (с тестами и без), самый быстрый:

mvn clean package -Dmaven.javadoc.skip=true -DskipTests -PskipBundlePlugin

Процесс сборки длился у меня где-то минут 6-7, так что в это время можно заварить себе чашечку чая изучить настройки сервера и приступить к написанию клиента.

Как я уже описывала ранее, открываем файл resources/quickfix.examples.ordermatch/ordermatch.cfg, проверяем SocketAcceptPort и заполняем поле TargetCompID нужным значением для нашего клиента (можно оставить BANZAI, которое указано по умолчанию, можно написать любое другое на ваше усмотрение):

[default]
FileStorePath=target/data/ordermatch
DataDictionary=FIX42.xml
SocketAcceptPort=9876 // порт для подключения
BeginString=FIX.4.2 // версия FIX 4.2

[session]
SenderCompID=EXEC // идентификатор сервера
TargetCompID=FIX_CLIENT // идентификатор клиента
ConnectionType=acceptor
StartTime=00:00:00
EndTime=00:00:00

Если хотите поменять значение идентификатора клиента, то лучше, конечно, сделать это перед сборкой, чтобы не пришлось собирать еще раз.

Когда сборка завершится, заходим в quickfixj\quickfixj-examples\ordermatch\target, проверяем, что там появились *.jar файлы:

Запускаем файл quickfixj-examples-ordermatch-2.2.0-SNAPSHOT-standalone.jar, так как он содержит все необходимые для запуска зависимости:

java -jar quickfixj-examples-ordermatch-2.2.0-SNAPSHOT-standalone.jar

Если появилась запись «Started QFJ Message Processor» – значит, сервер запустился. Проверьте, что в строке «Listening for connections at … [FIX4.2:EXEC->FIX_CLIENT]» указано нужное значение идентификатора клиента.

Структура проекта

Вот так выглядит готовый проект (стандартная структура веб-приложений: сервисы, контроллеры, модельки и т.д):

Создаем maven-проект со стандартными зависимостями и добавляем библиотеку QuickFix/J для работы с протоколом FIX:

<properties>
   <quickfixj.version>2.0.0</quickfixj.version>
</properties>

<dependency>
   <groupId>org.quickfixj</groupId>
   <artifactId>quickfixj-core</artifactId>
   <version>${quickfixj.version}</version>
</dependency>

<dependency>
   <groupId>org.quickfixj</groupId>
   <artifactId>quickfixj-messages-fix42</artifactId>
   <version>${quickfixj.version}</version>
</dependency>

Я подключила 2 модуля: quickfixj-core и quickfixj-messages-fix42 для работы с сообщениями только версии FIX 4.2.

Если в вашем приложении предполагаются сообщения различных версий протокола, можно подключить quickfixj-core + quickfixj-messages-all или просто quickfixj-all.

Полная версия pom.xml доступна в репозитории.

Настройка параметров подключения

По аналогии с файлом настроек на сервере, создадим файл resources/config/client.cfg с настройками нашего приложения.

В файле может быть один блок [default], в котором находятся параметры, общие для всех сессий, и несколько блоков [session] для описания параметров конкретной сессии (если сервер поддерживает сообщения различных версий протокола FIX, то для каждой версии создается отдельный блок [session]).

[default]
SenderCompID=FIX_CLIENT // идентификатор отправителя
TargetCompID=EXEC // идентификатор получателя
ConnectionType=initiator // приложение является клиентом
NonStopSession=Y
SocketConnectHost=localhost
ReconnectInterval=5
HeartBtInt=30
FileStorePath=target/data/banzai
UseDataDictionary=Y
DataDictionary=dictionary/fix4_2.xml
ValidateUserDefinedFields=N
AllowUnknownMsgFields=Y
ValidateUserDefinedFields=N
AllowUnknownMsgFields=Y

[session]
BeginString=FIX.4.2
ResetOnLogon=Y

Подробнее о параметрах

Начнем с блока [default]:

  • параметры сессии
    SenderCompID, TargetCompID – идентификатор отправителя и получателя сообщений соответственно (sender – наше приложение, target – сервер). Убедитесь, что эти значения совпадают со значениями параметров на сервере.
    ConnectionType (initiator/acceptor) – указывает, является наше приложение клиентом или сервером.
    — С помощью параметров StartTime и EndTime можно указать время начала и соответственно завершения работы сессии (например, биржа работает с 9.00 до 18.00, поэтому нет смысла запускать сессию вне этого времени). Если же сессия будет работать весь день, то можно указать NonStopSession=Y, что будет равносильно варианту StartTime=00:00:00 и EndTime=00:00:00.
    -параметры валидации сообщений
    UseDataDictionary=Y – можно использовать словарь сообщений, если вы работаете с биржей, спецификация сообщений которой отличается от стандартной (например, в словаре можно указать дополнительные теги или типы сообщений). При этом использование словаря обязательно, если есть сообщения с повторяющимися группами.
    DataDictionary – путь к словарю.
  • параметры клиента
    ReconnectInterval – интервал переподключения к серверу (в секундах).
    HeartBtInt – интервал проверочных сообщений типа HeartBeat (в секундах).
    LogonTimeout, LogoutTimeout – время ожидания Logon и Logout сообщений перед отключением сессии (в секундах).
    SocketConnectHost, SocketConnectPort – хост и порт подключения к acceptor-у.
  • параметры хранения сообщений и логов
    Сообщения и логи можно хранить в файлах или в базе данных (сообщения можно нигде не хранить, если выставить параметр PersistMessages=N).
    Я указала FileStorePath=target/data/banzai для хранения сообщений и номеров последовательностей в файле. Можно указать параметры базы данных (JdbcURL, JdbcUser, JdbcPassword и т.д), тогда сообщения будут храниться в базе данных.

В настройках конкретной сессии (в блоке [session]) главное – заполнить параметр BeginString, в котором указывается версия протокола FIX, использующегося в сообщениях.

Любые настройки можно указывать непосредственно при создании подключения в коде с помощью класса SessionSettings.

Подробнее о конфигурации клиента можно почитать в официальной документации.

Создание FIX-приложения

Теперь перейдем непосредственно к коду клиента. Чтобы создать FIX-приложение, нам нужно просто реализовать интерфейс Application:

public interface Application {
  void onCreate(SessionID sessionId);
  void onLogon(SessionID sessionId);
  void onLogout(SessionID sessionId);
  void toAdmin(Message message, SessionID sessionId);
  void toApp(Message message, SessionID sessionId)
    throws DoNotSend;
  void fromAdmin(Message message, SessionID sessionId)
    throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, RejectLogon;
  void fromApp(Message message, SessionID sessionId)
    throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType;
}

Эти методы вызываются в результате событий, происходящих в приложении (подробнее).

Метод fromApp срабатывает при получении сообщений с сервера, то есть в нем происходит основная логика. Остальные методы в основном служебные. Для удобства я создала абстрактный базовый класс BaseFixService, который реализует служебные методы интерфейса Application, и его наследника FixClientService, который занимается обработкой сообщений с сервера и соответственно реализует метод fromApp.

В приложении может быть установлено несколько сессий, поэтому в базовом классе будем хранить все сессии:

Map<SessionID, Session> sessions = new HashMap<>();

Так как метод onCreate срабатывает при создании новой сессии, в нем будем сохранять сессию по полученному ID с помощью метода lookupSession:

@Override
public void onCreate(SessionID sessionId) {
   log.info(">> onCreate for session: {}", sessionId);
   Session session = Session.lookupSession(sessionId);
   if (session != null) {
       sessions.put(sessionId, session);
   } else {
       log.warn("Requested session is not found.");
   }
}

Когда сессия отключается от сервера (мы завершили сеанс сообщением Logout или произошли какие-то технические проблемы и связь оборвалась), мы удаляем её из нашего хранилища.

@Override
public void onLogout(SessionID sessionId) {
   log.info(">> onLogout for session: {}", sessionId);
   sessions.remove(sessionId);
}

В FixClientService у нас находится главный обработчик сообщений – метод fromApp:

@Override
public void fromApp(Message message, SessionID sessionId) throws FieldNotFound, IncorrectDataFormat, IncorrectTagValue, UnsupportedMessageType {
   try {
       String type = MessageUtils.getMessageType(message.toString()); // получение типа сообщения
       switch (type) {
           case MARKET_DATA_SNAPSHOT_FULL_REFRESH:
               log.info("MarketData message: {}", message);
               break;
           case SECURITY_DEFINITION:
               log.info("SecurityDefinition message: {}", message);
               break;
           default:
               log.info("Unhandled message {} of type: {}", message, type);
       }
   } catch (Exception ex) {
       log.debug("Unexpected exception while processing message.", ex);
   }
}

С помощью класса MessageUtils библиотеки QuickFix/J можно получить тип входящего сообщения и далее обработать каждый случай (здесь для примера я указала несколько типов сообщений и вывела их в лог). В этой статье реализуем получение рыночных данных и их сохранение в кэш, остальные типы сообщений и их обработку более подробно разберем в следующих статьях и дополним логику нашего клиента.

Создание сервиса для подключения к серверу

Когда мы создали реализацию FIX-приложения, можно приступить к сервису для подключения к серверу – ConnectorService. При запуске приложения он будет создавать и запускать сокет для обмена сообщениями.

Для обмена сообщениями нужно создать SocketInitiator (на сервере аналогично создается SocketAcceptor). При создании передаются следующие параметры:

  • Application – FIX-приложение (т.е. класс, реализующий интерфейс Application, FixClientService в нашем случае)
  • MessageStoreFactory – способ хранения сообщений, это может быть, например, JdbcStoreFactory (хранение в базе данных), MemoryStoreFactory (хранение в памяти), FileStoreFactory (хранение в файле).
  • SessionSettings – настройки сессии, для их создания нужно передать файл с настройками (либо его название, либо InputStream).
  • LogFactory – хранение логов (аналогично сообщениям это может быть FileLogFactory, JdbcLogFactory), я использовала SLF4JLogFactory.
  • MessageFactory – используется для создания сообщений (можно использовать DefaultMessageFactory или MessageFactory для конкретной версии протокола FIX).

Путь к файлу настроек и дополнительные параметры (хост и порт подключения) для удобства я вынесла в конфигурацию приложения (application.yaml):

fix:
 cfg: 'classpath:config/client.cfg'
 socketConnectHost: localhost
 socketConnectPort: 9876

Соответственно при создании настроек сессии я использую этот файл и с помощью метода sessionSettings.set(String key, String value) добавляю параметры SocketConnectHost, SocketConnectPort:

try (InputStream inputStream = config.getCfg().getInputStream()) {
   SessionSettings sessionSettings = new SessionSettings(inputStream);
   sessionSettings.setString("SocketConnectHost", config.getSocketConnectHost());
   sessionSettings.setString("SocketConnectPort", config.getSocketConnectPort());

   MessageStoreFactory storeFactory = new FileStoreFactory(sessionSettings);
   SLF4JLogFactory logFactory = new SLF4JLogFactory(sessionSettings);
   MessageFactory messageFactory = new DefaultMessageFactory();

   socketInitiator = new SocketInitiator(fixClientService, storeFactory, sessionSettings, logFactory, messageFactory);
   socketInitiator.start();
} catch (Exception ex) {
   log.error("Exception while establishing connection to FIX server.", ex);
   throw new FixClientException("Exception while establishing connection to FIX server.", ex);
}

После создания настроек сессии объявляем LogFactory, MessageFactory, MessageStoreFactory и передаем их в конструктор SocketInitiator. Вызвав метод start() запустим подключение и сможем получать сообщения.

Не забудьте закрыть сокет при завершении работы с помощью метода stop().

Отправка запроса на получение рыночных данных

Когда приложение запустится и установится соединение с сервером, мы сможем отправлять и получать сообщения. Так как взаимодействие у нас построено на сокетах, отправка сообщения и получение ответа на него происходят асинхронно. Поэтому у нас будет два контроллера:

  • для инициации отправки сообщений
  • для получения данных, сохраненных в результате обработки ответов на отправленные ранее сообщения.

Чтобы получить рыночные данные (например, цену покупки, цену продажи инструмента), нам нужно отправить сообщение-запрос на данные и соответственно обработать ответное сообщение в методе fromApp.

Напишем метод для создания сообщения типа MarketDataRequest (о тегах сообщения можно почитать в спецификации).

public static Message createMarketDataRequest(String symbol) {
}

В библиотеке QuickFix/J все сообщения представляют собой классы, поля в которых соответствуют тегам. Можно создать экземпляр класса нужного нам сообщения и с помощью метода set() заполнить теги. Теги также представляют собой классы с обязательным полем FIELD, в котором хранится соответствующее числовое значение.

Например, тег symbol=55:

public class Symbol extends StringField {
   public static final int FIELD = 55;

   // constructors
}

Стандартные теги (соответствующие спецификации конкретной версии FIX) обычно можно заполнить напрямую. Например, для сообщения MarketDataRequest (далее буду сокращенно писать MDR) определены методы

set(SubscriptionRequestType value)
set(MarketDepth value)
set(Symbol value)
// ...

Если же при работе с конкретной биржей в сообщении присутствуют дополнительные теги, их можно задать с помощью общего метода setField(int key, Field<?> field): например, setField(5020, new IntField(10)) — добавим в сообщение тег <5020> со значением 10: 5020=10.

Создадим объект класса MarketDataRequest:

private static int mdReqID = 1;

MarketDataRequest marketDataRequest = new MarketDataRequest(
       new MDReqID(format("FixClient-%s", mdReqID++)),
       new SubscriptionRequestType(SNAPSHOT), //263
       new MarketDepth(1) //264, 1 = top of book
);

Подробнее о параметрах в конструкторе

В конструкторе передается три параметра:

  • MDReqID – уникальный в рамках данной сессии идентификатор сообщения.
  • SubscriptionRequestType:
    SNAPSHOT = ‘0’ (текущие данные);
    SNAPSHOT_PLUS_UPDATES = ‘1’ (текущие данные + подписка на обновление данных; при выборе этого типа каждый раз при изменении рыночных данных для инструмента, сервер будет отправлять сообщение типа MarketDataSnapshotFullRefresh с новыми данными);
    DISABLE_PREVIOUS_SNAPSHOT_PLUS_UPDATE_REQUEST = ‘2’ (отписка от получения данных + получение обновленных данных);
  • MarketDepth – глубина рынка для типа SNAPSHOT (цены формируются исходя из размещенных и ожидающих размещения заявок на покупку и продажу инструмента, эти заявки записываются в “книгу” заявок. Если указываем параметр равным 0 – будут учитываться все значения “книги”, если 1 – только “верхние” значения).

То же самое в виде сообщения: 262=FixClient-1 263=0 264=1.

Далее нужно указать параметры, которые мы хотим получить в результате запроса рыночных данных. Некоторые параметры в FIX-сообщениях задаются группами. При этом начинается такая часть сообщения с тега, в котором указывается количество последующих групп. В нашем случае параметр <267> NoMdEntryTypes хранит количество групп, а сами группы формируются из тегов <269> MdEntryType. Например, 269=0 означает, что мы хотим получить цену, по которой можно продать инструмент (Bid), а 269=1 – цену, по которой мы сможем купить инструмент (Ask, или Offer). Полный список стандартных значений этого тега можно посмотреть в спецификации. QuickFix/J автоматически заполняет в теге <267> количество параметров, мы можем только заполнить нужные нам поля и добавить каждую группу в сообщение:

   MarketDataRequest.NoMDEntryTypes group = new MarketDataRequest.NoMDEntryTypes(); //267

   group.set(new MDEntryType(MDEntryType.BID));
   marketDataRequest.addGroup(group);
   group.set(new MDEntryType(MDEntryType.OFFER));
   marketDataRequest.addGroup(group);

В сообщении будет выглядеть так: 267=2 269=0 269=1.

Можно делать MDR сразу для нескольких инструментов, в поле <146> NoRelatedSum передается их количество и далее заполняются группы тегов для каждого инструмента. Для простого запроса достаточно передать идентификатор инструмента в теге <55> (для более сложных запросов на фьючерсы или опционы нужно указывать дополнительные параметры, но для нашего базового случая это не нужно).

MarketDataRequest.NoRelatedSym instrument = new MarketDataRequest.NoRelatedSym();
instrument.set(new Symbol(symbol));
marketDataRequest.addGroup(instrument);

В сообщении: 146=1 55=AAPL.

Наш полученный MDR теперь можно отправить на сервер с помощью метода session.send():

@Override
public void sendMarkedDataRequest(String symbol) {
   sessions.forEach((sessionID, session) ->
           session.send(MsgUtils.createMarketDataRequest(symbol)));
}

Аналогично можно реализовать методы отправки любого другого сообщения (на создание заявки, на получение детальной информации об инструменте и т.д).

Для полученного метода отправки запроса на получение рыночных данных создадим REST endpoint, чтобы мы могли инициировать его отправку:

@PostMapping(value = "/market-data-request")
public void sendMarketDataRequest(@RequestParam("symbol") String symbol) {
   fixClientService.sendMarkedDataRequest(symbol);
}

Так будет выглядеть запрос, чтобы создать и отправить сообщение для получения данных об акциях Apple:
POST localhost:9090/fix-client/v1/market-data-request?symbol=APPL.

В результате будет отправлено сообщение:

8=FIX.4.2 9=117 35=V 34=3 49=FIX_CLIENT 52=20200601-17:10:34.103 56=EXEC 262=FixClient-1 263=0 264=1 146=1 55=AAPL 267=2 269=0 269=1 10=018

Обработка ответа и сохранение рыночных данных

Создадим отдельный сервис (MarketDataService), который будет обрабатывать рыночные данные, полученные в результате отправки запроса. Он будет сохранять полученные данные в объект, записывать их в память и отдавать при запросе по идентификатору инструмента.

Класс для хранения рыночных данных:

@Data
@Accessors(chain = true)
public class MarketDataModel {
   private String symbol;
   private BigDecimal bid;
   private BigDecimal ask;
}

Теперь нужно разобраться, как правильно обработать сообщение с данными и сохранить его.
Вот так выглядит сообщение, отправленное нам в ответ на запрос по символу AAPL:

8=FIX.4.2 9=104 35=W 34=3 49=EXEC 52=20200601-17:10:34.119 56=FIX_CLIENT 55=AAPL 262=FixClient-1 268=1 269=0 270=123.45 10=236.

Так как при запросе мы указывали группы параметров (Bid, Ask и т.д), разбирать сообщение тоже будем по группам:

message.getGroups(NoMDEntries.FIELD).forEach(group -> {
   int type = MsgUtils.getIntField(group, MDEntryType.FIELD).orElse(-1);
   BigDecimal value = MsgUtils.getDecimalField(group, MDEntryPx.FIELD).orElse(BigDecimal.ZERO);

   switch (type) {
       case 0:
           dataModel.setBid(value);
           break;
       case 1:
           dataModel.setAsk(value);
           break;
       default:
           log.warn("Invalid entry type: {}", type);
           break;
   }
});

В теге <269> хранится название параметра, а в теге <270> его значение. Соответственно, если тип параметра = 0 (т.е. Bid), то мы сохраняем значение соответствующего ему тега <270> в поле bid нашего объекта.

Далее проверяем тег <55> – идентификатор инструмента, и сохраняем по нему наши данные:

MsgUtils.getStrField(message, Symbol.FIELD).ifPresent(s -> {
   dataModel.setSymbol(s);
   marketData.put(s, dataModel);
});

Осталось только добавить сохранение данных в метод fromApp в случай обработки сообщения типа MarketDataSnapshotFullRefresh:

case MARKET_DATA_SNAPSHOT_FULL_REFRESH:
   marketDataService.saveMarketData(message);
   break;

Теперь при получении нашим приложением сообщения типа MarketDataSnapshotFullRefresh будет происходить обработка и сохранение данных в память приложения.

Соответственно в отдельный Rest-Controller добавляем метод получения данных по идентификатору:

@GetMapping
public ResponseEntity<MarketDataModel> getMarketData(@RequestParam("symbol") String symbol) {
   return new ResponseEntity<>(marketDataService.getMarketData(symbol), HttpStatus.OK);
}

Вызвав метод GET localhost:9090/fix-client/v1/market-data?symbol=AAPL
получим ответ:

{
  "symbol": "AAPL",
  "bid": 123.45,
  "ask": null
}

Запуск приложения

Наконец, можем запустить наше приложение, убедиться, что подключение к серверу осуществляется успешно, и попробовать отправить запрос на получение рыночных данных.

Если при запуске приложения в логах отображаются ошибки подключения (ConnectException), как на скриншоте ниже, проверьте, что сервер запущен и что вы указали правильные идентификаторы клиента и сервера и хост и порт для подключения:

В случае успешного запуска клиент и сервер должны обменяться Logon-сообщениями:

Отправим запрос POST localhost:9090/fix-client/v1/market-data-request?symbol=APPL, чтобы вызвать отправку сообщения MDR и убедимся, что сообщение действительно отправлено и ответ на него получен:

Бонус

Кстати, сообщения можно удобно парсить с помощью сайта – просто вставляете текст сообщения и получаете разбор по тегам и значениям:

Теперь вызвав метод GET localhost:9090/fix-client/v1/market-data?symbol=AAPL
мы должны получить ответ:

{
  "symbol": "AAPL",
  "bid": 123.45,
  "ask": null
}

Работает!

Конечно, на таком “игрушечном” примере далеко не уедешь, но для начала он хорошо подходит. Для более сложных примеров и для работы с условиями, приближенными к реальной бирже, можно получить доступ к тестовому контуру Московской биржи (MOEX) — для этого нужно оставить заявку на сайте. Я не нашла аналогичных тестовых контуров у других крупных бирж (именно для подключения напрямую через FIX-протокол), кроме симуляторов биржевой торговли, где выдаются виртуальные деньги и с помощью терминалов осуществляется торговля. Если знаете, где найти хороший тестовый сервер для работы по протоколу FIX, — поделитесь в комментариях, буду благодарна.

В следующей статье я планирую рассмотреть основные виды FIX-сообщений (соответственно дополнить приложение методами для их создания) и далее перейти к подробному рассмотрению процесса создания торговых заявок и их обработки биржей. Все примеры сообщений по-прежнему можно создавать с помощью приложения MiniFIX, если не хотите писать реализацию своего клиента.

Описание
Авторы

В закладки

Наш любимый симулятор фермерской жизни поддерживает мультиплеер, но как же играть в Farming Simulator 2019 по сети на пиратке? Тут мы вам расскажем про один способ.

Подготовка к запуску:
1. Скачиваем игру Farming Simulator 19 и устанавливаем ее в любую папку (6.16 ГБ): Скачать торрент-файлик.
2. Скачиваем и распаковываем Update 1.2.0.1 (734 Mb) в папку с игрой (скачать отдельно): ссылка (нажми на меня).
3. Если игра выдает ошибку или вылетает без причин, либо вы уже скачали игру с левого источника, но хотите играть в онлайне — используйте Fix Repair: Steam-Fix (8.31 Mb): Скачать. Фикс ставится путем распаковки в папку с игрой.

Запуск игры:
1. Запускаем Steam, заходим в свой профиль.
2. Запускаем игру через ярлык Farming Simulator 19 или FarmingSimulator2019Game.exe, который расположен в папке с игрой x64.

В игре:
Подключение:
Сетевая игра —> Войти в сессию —> Выбираем сервер —> Подключаемся к серверу друга или к любому другому —> Играем!
Создание сервера:
Сетевая игра —> Создать сессию —> Настраиваем всё по своему желанию и нажимаем Начать —> Ожидаем подключения других игроков —> Играем!

Примечания:
Для лучшего соединения между игроками рекомендуется использовать Russia — Moscow Steam Download Region (регион загрузки Steam).
Если вы решили воспользоваться данным руководством, то лучше создайте

отдельный аккаунт Steam

!

14-12-2018, 17:47

Скачать Farming Simulator 19

Подготовка к запуску:

  • Скачиваем игру Farming Simulator 19 и устанавливаем.
  • Если игра выдает ошибку или вылетает без причин, либо вы уже скачали игру с левого источника, но хотите играть в онлайне – используйте Fix Repair: Steam-Fix: Фикс ставится путем распаковки в папку с игрой

Запуск игры:

  • 1. Запускаем Steam, заходим в свой профиль.
  • 2. Запускаем игру через ярлык Farming Simulator 19 или FarmingSimulator2019Game.exe, который расположен в папке с игройx64.

В игре:

  • Подключение: Сетевая игра —> Войти в сессию —> Выбираем сервер —> Подключаемся к серверу друга или к любому другому —> Играем!
  • Создание сервера: Сетевая игра —> Создать сессию —> Настраиваем всё по своему желанию и нажимаем Начать —> Ожидаем подключения других игроков —> Играем!

Примечания:

  • Для лучшего соединения между игроками рекомендуется использовать Russia – Moscow Steam Download Region (регион загрузки Steam).
  • Для использования руководства лучше создать отдельный аккаунт Steam.

Если у Вас возникнут проблемы, пишите здесь в комментариях или в группе VK

Обновили FIX от 13.05.19

Скачать FIX с SharedMods Скачать FIX с ModsBase

3.6
13
голоса

Рейтинг статьи

Метки: farming simulator 19

Вам может также понравиться…

Комментарий удален модератором

Развернуть ветку

pUReflecT


Автор

значит freetp норм сайт получается? давно не видел кряки в самораспоковщике exe.файле. Я бы лучше ручками заменил нужные файлы)

Ответить

Развернуть ветку

Roanoac

Ну да. Я на нем штук 6 ко-опных игр с знакомым проходил. Проблем не было

Ответить

Развернуть ветку

pUReflecT


Автор

buratdb это что? в поиске не вижу)

Ответить

Развернуть ветку

Комментарий удален модератором

Развернуть ветку

Roanoac

Сейчас вот проходим рыцарей вротэма, все норм.

Ответить

Развернуть ветку

Mary Beckford

пользуюсь им время от времени, проблем еще не было ни с одной игрой)

Ответить

Развернуть ветку

pUReflecT


Автор

Я оригинальную игру уже проходил, захотелось освежить память, немного поиграть. Ну и можеть длс глянуть, а длс отдельно продается

Ответить

Развернуть ветку

Тень Тюленя

Я freetp пользуюсь тоже на постоянке, для онлайн фиксов, пока не нахватался ничего

Ответить

Развернуть ветку

Duhast

АХТУНГ! Нужно иметь ввиду, что настоящий сайт — фритп.орг, а в поиске (по крайней мере у меня и знакомых) выдаётся фритпс.клаб.
Там редирект на сайт с запароленным архивом, это вирус. Пожалуйста, я пожертвовал своим компьютером и проверил файл на вирустотале. 38/70.
На фритп.орг всё нормально, все фиксы у них устанавливаются собственным инсталлером, вирусов нет.

Ответить

Развернуть ветку

pUReflecT


Автор

Вроде сразу на норм сайт перешёл, редиректов не было. Уже пользуюсь им во всю, фикс на ремнант 2 работает исправно)

Ответить

Развернуть ветку

Duhast

Поздравляю! Сайт топовый, с друзьями всё оттуда качаем, если у кого-то лицухи нет.

Ответить

Развернуть ветку

GoldSrcFreeman

Пробуй открыть экзешник 7zip, если не сработает, делай виртуалку с семеркой/ХР и распаковывай всякую фигню там.

Ответить

Развернуть ветку

Комментарий удален модератором

Развернуть ветку

7 комментариев


Раскрывать всегда

Понравилась статья? Поделить с друзьями:
  • Динамометр эдр 20 инструкция по эксплуатации
  • Сеть окей руководство
  • Руководство по ремонту g30
  • Хай гир герметик для радиатора инструкция по применению
  • Силденафил сз 100 мг инструкция по применению таблетки взрослым