Меня зовут Денис Семёнов, я Senior Team Lead в Luxoft. Слаженная работа IT и банков сейчас кажется уже обычной. Мы привыкли делать переводы в один клик, ежедневно смотреть аналитику по своим инвестициям, пополнять вклады и считать затраты в приложениях. А что насчёт крупных клиентов банков? Я расскажу, как они проверяют данные по своим портфелям, при чём здесь оперативность и что могут улучшить программисты в финансовых расчётах.

Немного о нашем опыте

Мы сотрудничаем с одним из крупнейших международных банков около 10 лет, за это время было сделано несколько крутых и долгосрочных проектов по направлению бизнес-девелопмента. По требованию клиентов мы разрабатываем новые методологии расчёта маржина для различных типов ценных бумаг: акций, облигаций, опционов, фьючерсов и т. д. Среди технических задач — обновление фреймворков, миграция с устаревших библиотек на новые, работа с платформой, которая отвечает за маркетмейкинг — поддержание котировок в жизненном состоянии на рынках по отдельным специфичным видам продуктов. Об одном из проектов я сейчас расскажу.

Ускорение расчёта

В прошлом году к нам обратились с задачей под проект «10Х» = ускорение расчёта маржина в 10 раз для существующего объёма фондов.

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

Что было?

В среднем расчёт маржина составлял от 50 до 70 минут в зависимости от количества данных. На объём рыночных данных, которые получает и обрабатывает наша система, влияют объём торгов, волатильность на рынках и даже сезонность. 

Почему было важно ускорить время расчёта? Проблема в том, что, если в данные от наших апстримов закралась ошибка (например, отсутствовал курс обмена для какой-нибудь из мировых валют) и необходимо было срочно перепрогнать расчёт, то требовалось от полутора до трёх часов, чтобы исправить ошибку, перегрузить данные, перезапустить батч и предоставить новые отчёты. В таких экстренных условиях, когда счёт времени важен, 50 минут, которые требовал расчёт маржина, — это слишком много. 

Отсюда и возникла задача от топ-менеджмента — ускорить вычисления, насколько это возможно в рамках существующей архитектуры. С лёгкой руки нашего менеджера этот проект получил название Ten X (10Х) — ускорение в 10 раз (по аналогии с базой данных Times Ten от Oracle). Надо понимать, что название Ten X было взято из головы и первоначально никто не понимал, насколько удастся ускориться в реальности. Со стороны менеджмента банка эта задача нашла поддержку, и на её решение были выделены время и ресурсы. 

Почему так?

Архитектура системы была сделана таким образом, что сам расчёт клиентской группы дробился на множество отдельных вычислений, которые хранились в базе данных (у нас для этих целей использовалась Cassandra) так же, как и промежуточные результаты. Для того, чтобы выполнить шаг, сервер загружал данные из базы, производил вычисления и потом сохранял промежуточные результаты обратно. Таким образом операции ввода-вывода существенно замедляли общую производительность системы.

Хороша или плоха архитектура, доставшаяся нам от предыдущих разработчиков, — вопрос дискуссионный, но общая логика подсказывала, что не всё в порядке. В среднем у нас от 800 до 900 фондов, у каждого фонда максимум до 50 000 позиций (позиция — это конкретной ценная бумага, находящаяся на счету у клиента) — по современным меркам такой объём данных даже не близок к big data. Расчёт маржина для 4 миллионов позиций (по факту и того меньше) на пяти серверах не должен занимать 50 минут. Анализ этого вычислительного движка позволил найти места, которые можно улучшить, не переписывая его заново. Оставляя внутреннюю реализацию engine, мы изменили некоторые вещи, касающиеся хранения и использования калькуляции и связанных с ней объектов.

Что решили сделать?

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

Для этого мы сделали три шага:

1 шаг — вынос всей reference data (информация о составе продукта, цены, данные о торгах) на кэширующие сервера (Gemfire) и разогрев этих серверов перед батчем (batch) — непосредственным расчётом;

2 шаг — минимизация количества операций ввода-вывода для вычислений, при этом требовалось максимально задействовать оперативную память;

3 шаг — оптимизация памяти. Эта задача вытекала из предыдущей: если мы хотим держать всё в памяти, то использование памяти должно быть максимально эффективно. В архитектуре системы имелась одна проблема, которая при хранении объектов в базе данных была не очень существенной, но при хранении всех вычислений в памяти создавала ограничения по её расходу. Суть проблемы в следующем: если у нас есть ссылка из одного бизнес-объекта на другой (например, объект Position имеет ссылку на объект Product), то для хранения подобной ссылки в поле объекта резервируется количество памяти, равное размеру объекта. 

Каждый инстанс класса Position содержал в себе шэллоу-копию объекта Product, откуда нам нужен был только первичный ключ, а все остальные поля не использовались. Загрузка реального инстанса объекта происходила при вызове метода getProduct() у Position, который перехватывал аспект и, используя первичный ключ из продукта, запрашивал реальный объект из базы данных или из Gemfire-кэша. На примере объекта Product мы увидели, насколько данная схема хранения ссылок между объектами неэффективна: класс Product имел более 100 различных полей, но реально использовалось только 16 байт, зарезервированных для UUID. 

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

В нашем примере с классом Position в поле product теперь помещался инстанс динамически созданного класса Product$1, который реализовывал интерфейс Product. Таким образом экономия только на одном объекте составляла сотни байтов, но учитывая, что подобным образом мы изменили способ хранения ссылок между всеми бизнес-объектами, то экономия шла уже на десятки мегабайт. 

Кроме того, мы ещё добавили пул для хранения ссылочных объектов, и теперь, если у нас было несколько объектов, ссылающихся на бизнес-объект типа Product c одинаковым ключом, то все они использовали тот же самый инстанс класса Product$1. Данная оптимизация также позволила обрабатывать даже самые большие фонды с размером памяти для JVM (heap) в 8 ГБ. Это принесло и другое преимущество, так как в скорости наше приложение должно было быть мигрировано в банковский k8s-клауд, в котором существующее ограничение для докер-контейнера составляет 8 ГБ. 

Как стало?

После того, как все три вышеизложенных шага были реализованы, время расчёта батча для всех клиентов сократилось с 50–60 минут до 5–8 минут в зависимости от объёма рыночных данных на текущий день. Собственно при возникновении необходимости перезапуск всех наших вычислений из-за неправильных данных от апстримов превратился из долгого ожидания в достаточно рутинную операцию, в которой подготовка к перезагрузке данных в базу иногда занимает больше времени, чем собственно вычисление. 

В итоге мы ускорили расчёт в 10 раз и выполнили задачу, как и хотели клиенты. Не зная изначально, что будет в результате, мы сделали 10Х. 

Для кого делали проект?

Это проект для клиентов, чьим основным брокером является наш банк. У нас есть SLA (service level agreement) — согласно ему, до 7:00 утра по времени США у нас должны быть готовы отчёты о размерах маржина, доступного для наших клиентов. Если они недоступны, то возможны штрафные санкции — выплаты клиентам со стороны банка. 

Кто клиент?

Клиентами банка являются крупные хедж-фонды, размер инвестиций которых может достигать десятки миллиардов долларов. Т. е. это не индивидуальный инвестор, который собрал несколько тысяч и хочет попробовать себя на фондовом рынке (для таких существуют различные брокеры типа Interactive Brokers), а компания с сотнями сотрудников, которая управляет финансами и пенсионными накоплениями миллионов граждан США и всего мира.

Где клиент сталкивается с нашим решением?

Для клиентов банка есть портал, где можно посмотреть ежедневный финансовый отчёт, который говорит о том, сколько денег они должны держать на счетах в банке, чтобы в случае ликвидации портфеля по какой-то причине банк не понёс убытки. Настройка параметров маржин-методологий подбирается индивидуально для каждого клиента и является строго конфиденциальной (как впрочем, и всё остальное, начиная с имени клиента и состава его портфеля до размера маржина, который он платит банку, и размера капитала на счетах). 

Методология расчёта, если говорить про США, состоит из двух частей: minimum — то, что спускается регулятором, FINRA (Агентство по регулированию деятельности финансовых институтов США), которое требует минимальный размер маржина на определённый размер портфеля, и house — внутренние правила банка. К примеру, пользователь — риск-менеджер — может, меняя параметры house-методологии, запускать вычисления и смотреть, как изменится маржин, если он чуть ослабит требования для определённых типов финансовых инструментов.

Помимо вычислений, которые идут в пакетном батче — когда мы делаем расчёт для всех клиентов — есть ещё и индивидуальные вычисления. К примеру, в ситуации, когда тот же риск-менеджер хочет проанализировать состояние портфеля клиента и запускает вычисления только для него.

Кто занимался проектом?

Решением занималось восемь человек из команды: Java-разработчики, PM, тестировщики. Задача QA была в том, чтобы мы ничего не сломали, не внесли регрессию в результате изменений в архитектуре.

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

Хотите разбираться в банковских проектах и влиять на развитие финансового сектора? Узнайте больше об открытых вакансиях Luxoft и нашей команде по ссылке.

Комментарии (12)


  1. baldr
    23.09.2021 14:58
    +3

    Senior Team Lead в Luxoft

    Ну вот это уже звания пошли! А Junior Team Lead у вас там тоже есть?


    1. LuggerMan
      23.09.2021 15:31
      +3

      Intern Lead For Senior Teams of Junior Developers


    1. spdenis Автор
      23.09.2021 16:56
      +2

      Чем больше компания тем больше штатное расписание и тем больше всяких должностей. "Старший руководитель группы" - звучит тоже не очень )


      1. mrbald
        24.09.2021 11:42

        Звание - как звание. Нужно же чтобы у новобранцев цель была.


  1. baldr
    23.09.2021 15:44
    +1

    В принципе, статья хорошая, но смысл можно выделить так: ORM вытаскивал данные из базы для вычислений неоптимально, поэтому мы создали облегченные классы и стало оптимально. Ну, и еще подкрутили ссылки на объекты.

    Не мешало бы еще добавить описание стека на котором все это крутится. Я понимаю - банк, NDA, все дела. Но, судя по всему, это Java? Hadoop/Spark или что-то самописное?

    Деталей по хранению и реализации очень мало, но складывается ощущение, что если заранее вытащить часть данных, например, в реляционную базу (Postgres/ClickHouse), то можно посчитать все через SQL и из коробки получить как связи по ключам, так и индексы и вычисления в памяти (как решит планировщик). При грамотном запросе он еще и параллелиться будет. Нет вопросов к хранению данных в Cassandra - наверняка есть причины (шардинг, распределенность, кластеры и тп).


    1. spdenis Автор
      23.09.2021 17:02
      +1

      По стеку технологий: Java 8, Spring (Core, Data), Hibernate, Oracle, Cassandra. Движок для распределенных вычислений - внутренняя разработка банка. Да, большинство объектов вытаскивается в память или через SQL из Oracle или Rest. Но посчитать что либо на SQL  представляется мало реальным, уж больно запутанная бизнес логика.


      1. Stas911
        25.09.2021 02:49

        А в Hibernate нельзя было lazy loaded fields как-то задать, чтобы не все поля выбирались? Давно не смотрел, но даже в древнем это вроде было.


        1. spdenis Автор
          25.09.2021 08:52

          Дело в том, что бизнес уровень не работает напрямую с сущностями из Hibernate. Hibernate просто один из источников данных (наравне с вебсервисами) которые впоследствии конвертируются в внутренние модельные бизнес объекты, которые уже используются для вычислений. Сам вычислительный движок умеет сохранять и загружать модельные объекты в Cassandra. Что касается lazy лоад то в хибернейте он есть только для связанных сущностей, для полей нет.


    1. bankir1980
      25.09.2021 00:23

      Есть такая банковская система RS-Bank. 5-я версия, год примерно 2007-й. Модуль отчётности для ЦБ, свежая форма 251 ежемесячная. Так вот. Расчет формы в банке, который даже в топ 200 не входил, выполнялся 40 минут. Залез в код, увидел студенческую лапшу, переписал - в результате расчет стал меньше минуты. До сих пор вспоминаю - как такой крупный разработчик, как R-Style Softlab допустили такое? Потом, конечно, они всё поправили, но осадочек остался...

      Это я к тому, что не всегда оптимизация - это достижение. Иногда - это просто когда переделали работу по нормальному :)


      1. spdenis Автор
        25.09.2021 09:13

        Ну в данном случае речь не идет о том что кто-то не знал алгоритмов и структур данных и использовал n^2 там где нужно было использовать хэш-мапу. Сам вычислительный энжин написан достаточно хорошо, но изначально был нацелен на тесное взаимодействие с базой данных во время вычислений. Так что это больше похоже на перенос существующего кода на новую платформу у которой вместо базы данных используется память, но в тоже время сам бизнес уровень остался неизменным.


  1. mrbald
    24.09.2021 11:31
    -1

    Интересно было бы сравнить со считалкой на Pandas "на коленке". Там не понадобятся никакие caches. Само вычисление для указанного количества позиций должно занимать несколько секунд). Загрузка всего необходимого из базы отфильтрованного по value-date тоже вроде должно шустро быть -- Oracle жмет пару сотен тысяч tuples/second не потея, когда запрос без багов. Откуда минуты получаются - интересно.

    Приходилось чинить такие вещи, после Люпама в том числе, основная проблема там в agile development по методу сканирующего окна (почти как в том анекдоте, где сначала пишут слово ХУХь а затем прибивают к нему доски). У программистов тупо недостаточно инфы, чтобы всё сделать правильно, потому что BAs/PMs и сами еще не знают, так как они -- тоже аутсорсеры, а те, кто знают всё про механику управления портфелями называются portfolio managers, сидят в другом здании, и рады бы помочь, да никто не спрашивает, чтобы не показать некомпетентность (иначе контракт могут не продлить).


    1. spdenis Автор
      25.09.2021 09:05
      +1

      Минуты получаются из-за запутанной бизнес логики. Например чтобы посчитать маржин для акции на ETF (exchange-traded fund) необходимо выполнить его декомпозицию на конституенты. И получается вместо одной ценной бумаги мы уже имеем несколько сотен и каждая со своими правилами маржина. Также есть логика по объединению бумаг из портфеля в группы чтобы предоставить клиенту уменьшение маржина за счет хэджирования внутри портфеля или допустим хэджирования между конституентами различных ETF. И это только один пример. Я это к тому, что выполнять вычисления на стороне базы данных слишком сложно, так как требуется постоянное прыгание между исходными и промежуточными данными, а правила расчета маржина слишком нелинейны.