Необходимо написать простую биржу с клиент-серверной архитектурой. Для упрощения задачи, биржа будет торговать только долларами (USD) за рубли (RUB).
Сервер должен принимать заявки на покупку или продажу долларов за рубли от разных клиентов по определённой цене.
В случае, если цена на покупку и цена на продажу у нескольких клиентов пересекается — нужно заключать сделки между ними. В этом случае купленный объём будет зачисляться на баланс клиентам.
- Поддерживает подключения нескольких клиентов одновременно.
- Даёт возможность просмотра клиентом всех своих активных торговых заявок, всех своих совершенных сделок и своего баланса.
- Подключается к серверу и реализует все его возможности.
- Торговая заявка содержит объём, цену и сторону (покупка/продажа).
- Торговая заявка активна до тех пор, пока по ней есть активный объём.
- Если две заявки пересекаются по цене — для сделки выбирается цена более ранней заявки.
- Если заявка пересекается с несколькими заявками по другой стороне — приоритет в исполнении отдаётся заявкам с максимальной ценой для заявок на покупку и с минимальной ценой для заявок на продажу.
- Возможно частичное исполнение заявки. (см. пример)
- Баланс клиента не ограничен — он может торговать в минус.
- Пользователь 1 выставил заявку на покупку 10 USD за RUB по цене 62.
- Пользователь 2 выставил заявку на покупку 20 USD за RUB по цене 63.
- Пользователь 3 выставил заявку на продажу 50 USD за RUB по цене 61.
- Сервер сматчил эти заявки между собой. Получилась две сделки:
- На 20$ по цене 63 между П2 и П3.
- На 10$ по цене 62 между П1 и П3.
- Теперь на балансе пользователей:
- Пользователь 1: 10 USD, -620 RUB.
- Пользователь 2: 20 USD, -1260 RUB.
- Пользователь 3: -30 USD, 1880 RUB.
- Торговая заявка пользователя 3 (частично исполненная на 30$) осталась активной на 20$.
|
⚠️ Важно! Мы исполняемся в таком порядке неслучайно: начали с заявки П2, поскольку её цена больше, чем у П1.
- Возможность просмотра клиентом котировок 📈
- Получение отчёта о совершённой сделке в момент её заключения всеми сторонами-участниками. 🤼
- Возможность отмены активных заявок клиентами. 🚫
- Сохранение истории всех заявок и сделок в базу данных (например, PostgreSQL). 💽
- Авторизацию клиента с паролем. 🔑
- GUI к бирже на QT. 🤯
- Клиент может представлять собой обычное консольное приложение.
- Все основные функции должны быть покрыты тестами.
- Проект должен быть написан с использованием C++17/20.
- Разрешается использование сторонних библиотек.
- Проект должен собираться с использованием CMake.
- Описание, инструкция по сборке и инструкция по использованию проекта должны быть в файле README.md
- Проект должен быть выполнен в git-репозитории, ссылку на который необходимо предоставить.
Реализованные Бонусы
бонус | реализовано? |
---|---|
Возможность просмотра клиентом котировок 📈 | на сервере - да, на клиенте - нет |
Получение отчёта о совершённой сделке в момент её заключения всеми сторонами-участниками. 🤼 | нет |
Возможность отмены активных заявок клиентами. 🚫 | да |
Сохранение истории всех заявок и сделок в базу данных (например, PostgreSQL). 💽 | да |
Авторизацию клиента с паролем. 🔑 | да |
GUI к бирже на QT. 🤯 | да |
Реализовано на Boost.
Сборка через cmake и запуск в докере, реализовано хранение данных в PostgreSQL, хранение токенов сессий для каждого пользователя в Redis.
При авторизации отправляется токен сессии для пользователя и последующие запросы на сервер происходят по токену сессии.
таблицы:
users | deals |
---|---|
--------------- | --------------- |
id | id |
first_name | from_user_id |
second_name | to_user_id |
balance_USD | deal_type |
balance_RUB | amount |
login | exchange_rate |
password | deal_status |
time_start | |
time_end |
key | value |
---|---|
user:<user_id>:token |
Создает заявку.
Headers:
key | value |
---|---|
Token | OppAqpglzvrC17UQWCYqscHrNs6DfVFMRSKCQWK5UKOxOKpd74Z86QUEml24wSiQ3i9Fh2CLnBcK0KHVLY3DTyj8s5ujEphpqfHGSU6pnljmGEXmp2uqLxwkOwqrZ6Xy |
User_id | 19 |
Body:
{
"deal_type": "buy",
"amount": "100",
"exchange_rate": "60"
}
Ничего не возвращает.
Отменяет заявку.
Headers:
key | value |
---|---|
Token | OppAqpglzvrC17UQWCYqscHrNs6DfVFMRSKCQWK5UKOxOKpd74Z86QUEml24wSiQ3i9Fh2CLnBcK0KHVLY3DTyj8s5ujEphpqfHGSU6pnljmGEXmp2uqLxwkOwqrZ6Xy |
User_id | 19 |
Body:
{
"deal_id": "7"
}
Ничего не возвращает.
Авторизация по логину и паролю, если успешно, то возвращает token и id пользователя.
Headers: none
Body:
{
"login": "1",
"password": "1",
"remember_me": "1"
}
(с remember_me была идея создавать временный токен, по которому после сессии нельзя было зайти)
Reply: Если успешно
{
"token":"gVfIhbBitZ5N746SBzFy42yAlmUaYbslAKw9ttaLzNPGafkyBtTzwWA0vzAYr7CQ011HoRykXA7Fddk8ikXKdiG8YFVqEE5LyW2JJZlXTaM9WOMTccbfwlGbnDT8S7Md",
"user_id":"24"
}
Если неуспешно
{
"Wrong login or password"
}
Регистрация по логину и паролю, если успешно, то возвращает token и id пользователя.
Headers: none
Body:
{
"first_name": "vanya",
"second_name": "vasiliev",
"login": "ivan2",
"password": "crazypassword",
"remember_me": "0"
}
(с remember_me была идея создавать временный токен, по которому после сессии нельзя было зайти)
Reply: Если успешно
{
"token":"gVfIhbBitZ5N746SBzFy42yAlmUaYbslAKw9ttaLzNPGafkyBtTzwWA0vzAYr7CQ011HoRykXA7Fddk8ikXKdiG8YFVqEE5LyW2JJZlXTaM9WOMTccbfwlGbnDT8S7Md",
"user_id":"24"
}
Если неуспешно
{
"User already exists"
}
Вот как раз это для remember_me
Headers:
key | value |
---|---|
Token | OppAqpglzvrC17UQWCYqscHrNs6DfVFMRSKCQWK5UKOxOKpd74Z86QUEml24wSiQ3i9Fh2CLnBcK0KHVLY3DTyj8s5ujEphpqfHGSU6pnljmGEXmp2uqLxwkOwqrZ6Xy |
User_id | 19 |
Reply: Если успешно
{
"is_user_valid": "1"
}
Если неуспешно
{
"is_user_valid": "0"
}
Запрос на получение текущих активных заявок
Headers:
key | value |
---|---|
Token | OppAqpglzvrC17UQWCYqscHrNs6DfVFMRSKCQWK5UKOxOKpd74Z86QUEml24wSiQ3i9Fh2CLnBcK0KHVLY3DTyj8s5ujEphpqfHGSU6pnljmGEXmp2uqLxwkOwqrZ6Xy |
User_id | 19 |
Reply:
[
{
"amount": "100",
"deal_status": "active",
"deal_type": "buy",
"exchange_rate": "60",
"from_user_id": "1",
"id": "2",
"time_start": "2023-12-19 09:41:32.716872",
"to_user_id": "2"
},
...
{
"amount": "100",
"deal_status": "active",
"deal_type": "buy",
"exchange_rate": "60",
"from_user_id": "1",
"id": "4",
"time_start": "2023-12-19 09:45:08.023111",
"to_user_id": "2"
}
]
Запрос на получение заявок пользователя
Headers:
key | value |
---|---|
Token | OppAqpglzvrC17UQWCYqscHrNs6DfVFMRSKCQWK5UKOxOKpd74Z86QUEml24wSiQ3i9Fh2CLnBcK0KHVLY3DTyj8s5ujEphpqfHGSU6pnljmGEXmp2uqLxwkOwqrZ6Xy |
User_id | 19 |
Reply:
[
{
"amount": "100",
"deal_status": "active",
"deal_type": "buy",
"exchange_rate": "60",
"from_user_id": "19",
"id": "6",
"time_end": "",
"time_start": "2023-12-23 02:49:30.994991",
"to_user_id": "0"
},
...
{
"amount": "100",
"deal_status": "active",
"deal_type": "buy",
"exchange_rate": "60",
"from_user_id": "19",
"id": "8",
"time_end": "",
"time_start": "2023-12-23 02:49:52.492691",
"to_user_id": "0"
}
]
Запрос на получение информации о пользователе(имя, фамилия, баланс)
Headers:
key | value |
---|---|
Token | OppAqpglzvrC17UQWCYqscHrNs6DfVFMRSKCQWK5UKOxOKpd74Z86QUEml24wSiQ3i9Fh2CLnBcK0KHVLY3DTyj8s5ujEphpqfHGSU6pnljmGEXmp2uqLxwkOwqrZ6Xy |
User_id | 19 |
Reply:
{
"balance_rub": "0",
"balance_usd": "0",
"first_name": "vanya",
"second_name": "vasiliev"
}
Запрос на получение данных для построения котировок
Headers:
key | value |
---|---|
Token | OppAqpglzvrC17UQWCYqscHrNs6DfVFMRSKCQWK5UKOxOKpd74Z86QUEml24wSiQ3i9Fh2CLnBcK0KHVLY3DTyj8s5ujEphpqfHGSU6pnljmGEXmp2uqLxwkOwqrZ6Xy |
User_id | 19 |
Reply:
[
{
"exchange_rate": "34",
"time_start": "2023-12-26 17:12:44.614782"
},
{
"exchange_rate": "223",
"time_start": "2023-12-26 10:30:09.61555"
},
...
{
"exchange_rate": "222",
"time_start": "2023-12-26 10:28:39.898575"
}
]
Реализовано на Qt5.
Сборка через cmake.
"Remember me" не работает на клиенте, но реализация на бэкэнде есть, так же как и с котировками.
Для работы всего нужно поставить
sudo apt install -y make docker docker-compose postgresql libboost-dev libboost-program-options-dev cmake build-essential libpq-dev libhiredis-dev qt5-base-dev
Далее склонируйте репозиторий и перейдите в него
git clone https://github.com/d33fur/test-ntprogress.git && cd test-ntprogress
Запуск
make all
Для очистки и выключения контейнеров
make clear-all
Логи бэкэнда
make logs-server
Больше комманд можно увидеть внутри Makefile
P.S. в связи с зачетной неделей и предновогодними приготовлениями не успел исправить ошибку сегментации при отправке ответа с бэкэнда и написать тесты.
Из за ошибки сегментации бэк нужно запускать отдельно
make backend-all
P.P.S.
- Починил ошибку сегментации, теперь можно запускать через
make all
. - Благодаря тестам нашел несколько ошибок и исправил их.
- Написал тесты, запуск записал в
make all
, но можно и отдельно запускать(подробнее в Makefile). - Тесты на логику биржи можно было покомпактнее написать.
- Для запуска
make backend-all
,build-backend
иstart-backend
необходимо поменять айпи баз данных в коде с их айпи в сети докера на локалку. - Структурировал проект. Клиент выглядит вроде неплохо, но в бэке 35+ функций в одном файле - эт плохо. Можно было декомпозировать, но я этого не сделал.