Биржа криптовалюты своими руками, или как мужики crypto бота разрабатывали

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
@CREXS_bot
@CREXS_bot

Вступление

Начать хотелось бы с того, что я всегда чувствовал, что где-то внутри меня в страшных муках погибает писатель. Еще в школе гуманитарные науки давались лучше и вызывали больший интерес чем науки точные. Но вот уже на висках появляется кроткая седина, а я так же глубоко в IT, как далеко от литературы. Страшно вспомнить тот путь, который пришлось пройти, и тот код, который необходимо было через себя пропустить. Как-то всегда все в основном крутилось вокруг денег: эти деньги кто-то кому-то куда отправлял бесконечно, какие-то вечные транзакции, постоянные комиссии, списания и зачисления. Этот безумный вальс закружил так сильно, что уже сложно сказать, когда впервые в голову пришла идея реализации p2p и биржи. Возможно влияние оказал кратковременный опыт взаимодействия с протоколом MT4, возможно сказались аукционы недвижимости, в разработке которых я принимал участие, а может быть и просто рынок ценных бумаг - сейчас уже вообще не разберешься как все докатилось до сегодняшних реалий. Но факт остается фактом: вместо того, чтобы писать захватывающую книгу про приключения и сокровища, приходится рассуждать о менее веселом материале про криптовалютную биржу.

Пока мы не добрались до основной темы, хочется подчеркнуть некоторые важные детали, на которые стоило бы обратить внимание:

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

  2. Не принимать за аксиому: все умозаключения в этой заметке основываются исключительно на собственной практике и личном опыте, а значит они максимально субъективны и воспринимать их следует через призму недоверия в порядке убывания собственных приоритетов.

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

  4. Код в материале будет только в разделе тестирования и будет его там совсем не много, однако он довольно наглядно отражает суть происходящего.

Давайте смотреть правде в глаза: все хотят себе крипто биржу. Многие даже толком не понимают, что это такое, но точно где-то на уровне подсознания чувствуют, что это - именно то, что им нужно. Почему? Ну это модно, молодежно и не нужно ничего делать - только загребать деньги лопатой и раскидывать по мешкам. Так что запасайтесь мешками и лопатами, присаживайтесь по-удобнее: сейчас мы с вами наконец-то разберемся раз и на всегда что это за стакан такой, который должен любить каждый настоящий мужик.

Окружение

Начнем с того, что я не один. Не пытайтесь повторить это в одиночку - у вас ничего не выйдет. Самое первое, и самое важное, из того, что мне удалось понять: нужна команда и налаженные бизнес процессы. Вы можете быть крутым разработчиком на супер skills, но этого все равно не будет достаточно. На это есть ряд причин:

  • Каждый должен заниматься своим делом.

  • Всегда есть люди, которые что-то делают лучше вас.

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

Ваше окружение - это самое важное. Это касается людей, оборудования и техники, операционной системы, инструментария. Должны быть налажены механизмы QA и CI. Должны быть ответственные за тех. поддержку, и те, кто отвечают за контент и продвижение. А еще способные создавать качественный медиа контент. Можно начать долго перечислять, но лучше давайте остановимся.

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

Чем больше я обо всем этом думаю, тем тверже становится уверенность в том, что единственное, что всегда присутствует в любом окружении - это непредвиденные обстоятельства. Окружение не может соответствовать ожиданиям. И один из главных навыков, который удалось приобрести за долгие годы практики - это умение ошибаться, но учиться на своих ошибках. Не ошибается тот, кто ничего не делает, как бы банально это не звучало, но ошибки будут главным фактором в нашем окружении, а наша задача будет свести их до минимума.

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

Есть желание проговорить и обдумать некоторые вещи, которые сложнее и важнее чем какой-то код. Чтобы писать код - много ума не нужно, ведь самое сложное - это знать не как писать код, а какой именно код писать. Абсолютно идентично можно выразиться и про любой текстовый материал, например этот.

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

Любой, даже начинающий трейдер, вам скажет, что для торговле на бирже у вас не должно быть долгов. Должны быть цели, должна быть стратегия, должна быть подушка безопасности. Про стратегию поговорим в разделе продвижения, а вот на счет подушки безопасности, следует подумать самостоятельно. Если вы собираетесь делать биржу - вы должны уметь как минимум торговать на ней. Причем желательно не ограничивать себя скудным рынком криптовалюты, а пробовать ценные бумаги и металлы. Откройте брокерский счет - это хорошая практика и в любом случае она принесет плоды, в виде, как минимум, повышения финансовой грамотности. Распределение активов - важный момент не только для игры с курсами но и вообще для вашего окружения в целом.

В окружении понадобится тестовый бот, который работает на тестовых блокчейнах. Это позволит производить QA. Монеты в этих блокчейнах можно добыть из кранов, где-то это происходит в discord, как в случае с TRX и Tether TRC20, где-то в телеге, как в случае с TON, а где-то вообще придется поковыряться в поисках. Мы так же имеем тестовый бот, который можно погонять без реальных монет в условиях, приближенных к боевым:
@CREXStest_bot

P2P

peer to peer
peer to peer

Два человека на огромной планете - какова вероятность, что они найдут друг-друга? Сейчас из за нейронных сетей уже нет твердой уверенности в том, что ты не робот, друг. В этой части, хочется немного упомянуть, что взаимодействие между разными объектами - всегда очень относительная задача. И это может быть достаточно сложным процессом, особенно если объектов этих много и качество у них разное.

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

Таким образом мы определили две важные составляющие p2p - это поиск объектов взаимодействия и оценка их состояния. Важные элементы, на которые стоит обратить внимание. Например если peer1 хочет купить TRX у peer2, но peer2 в этот момент меняется с peer3, в таком случае peer1 должен получить отказ, т.к. состояния объектов не совместимы в текущий момент времени и объекты не пригодны для взаимодействия.

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

Давайте оценим свое состояние - какое оно сейчас. В порядке? Когда все дела в порядке - и состояние в покое. Так же и в p2p обмене - важно найти связи с объектами, у которых все дела сделаны и они готовы к торгам.

Если простыми словами, то обменник - это возможность менять крипту на фиат, а биржа - крипту на крипту. Например у peer1 есть немного TRX и он хотел бы поменять их на ETH в виду каких-то личных потребностей, и так сложилось, что где-то на планете в этот момент сидел peer2, у которого как раз были эти самые ETH и он в принципе, по своему состоянию, был не против поменять их на TRX. Ребята начали меняться, но тут оказалось, что peer1 надо немного больше ETH, чем есть у peer2. Совершился обмен на ту сумму, что была и разбежались, но peer1 остался в состоянии, когда у него еще не все проблемы решены - ему нужно еще немного ETH.. А больше ни у кого нет. Это немного грустно, но тут на помощь приходит стакан.

Закидываем заявку peer1 в стакан и пусть он пока там бултыхается со своими ETH. Придет его время, ведь не стоит забывать про курс, который является главным флагом для любого трейдера. Наполняем стакан разными заявками. И по стаканчику для каждой пары. Звучит красиво - прям как застолье на юбилей.

Очень важен порядок следования. Получается, что если BTC сидит слева, а USDT справа, то лучше обговорить это заранее, в противном случае, они потом перепутаются и у нас начнутся проблемы.

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

Чтобы понять, как растёт количество пар, рассмотрим следующее:

  1. Когда у вас есть n валют, количество возможных пар без учёта порядка и без повторений равно n*(n-1)/2​.

  2. Когда вы добавляете одну новую валюту, она образует пары с каждой из n уже существующих валют. Таким образом, добавляется n новых пар.

При добавлении новой валюты к n существующим, общее количество пар увеличивается с n*(n-1)/2 до (n+1)*n/2. Если, например с 4 валютами, у нас было 6 торговых пар. После добавления ещё одной валюты, общее количество торговых пар увеличивается до 10.

Если же по какой-то причине окажется, что есть возможность использовать не только BTC/USDT пару но и USDT/BTC, то количество возможных комбинаций, при добавлении пятой валюты вырастет до 20, что кратно увеличивает сложность.

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

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

Весь процесс грубо можно поделить на несколько общих этапов:

  1. Сбор и подготовка состояния пользователя

  2. Формирование биржевой заявки

  3. Поиск и исполнение подходящих заявок

  4. Создание платежных документов

  5. Информирование пользователей и администраторов

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

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

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

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

Ну и на финальном шаге - отправляем оповещения всем участникам шоу, чтобы они были в курсе.

Тестирование

/test
/test

Без тестирования тут явно ничего не получится. Здесь будет немного примеров, как мы тестируем функциональность обмена, но это лишь малая часть того, что уже покрыто тестами и совсем крупица от того, что еще предстоит покрыть. В коде достаточно много комментариев, так что для того, чтобы не дублировать информацию не буду разбирать эти кейсы отдельно.

def test_direct_exchange_step7(self):
        this = DirectExchangeFinalHandler  # обработчик тот же, что и на шаге 6
        create_transaction_function_path = 'bot.support.billing.Billing.create_transaction'

        def __make_prototype_order(amount: int) -> DirectExchangeOrder:
            prototype_order = self.find()[0]
            prototype_order.profile = self.profile2
            prototype_order.already_exchanged_amount = Decimal('0')
            prototype_order.amount = Decimal(str(amount))
            prototype_order.price = Decimal('0.00000240')
            prototype_order.order_type = DirectExchangeOrder.Types.SELL
            prototype_order.save()
            return prototype_order

        def __copy_prototype_order(prototype_order: DirectExchangeOrder, copy_numbers: int):
            for i in range(copy_numbers):
                prototype_order.pk = None
                prototype_order.save()

        # тестируем бизнес логику обмена

        # удалим все завявки
        for a in self.find():
            a.delete()

        # и сделаем 1 новую на продажу 100 TRX по цене 0.00000240 BTC
        self.offer = self.create_order(
            profile=self.profile2,
            crypto_from=self.crypto2,
            crypto_to=self.crypto1,
            amount=self.amount,
            price=self.trx_btc_price,
            order_type='sell',
        )

        self.update.callback_query["data"] = f'{this.__name__}|{self.offer.pk}|next'
        self.dp.update = self.update

        self.dp.machine.set_selected_values(profile=self.profile, key='stock_selected_order', value=self.offer.pk)
        self.dp.machine.set_selected_values(profile=self.profile, key='stock_price', value=self.offer.price)

        # протестируем покупку
        self.dp.machine.set_selected_values(profile=self.profile, key='stock_type', value=self.StockType.BUY)
        # случай 1: недостаточно средств, цена подходит
        with patch(self.__get_balance_function(is_buy=True), return_value=Decimal('0.000240')):
            with patch(create_transaction_function_path) as mock_create_transaction_function:
                with patch('bot.handlers.base.BaseHandler.answer') as answer_mock_function:
                    self.dp.machine.set_selected_values(
                        profile=self.profile, key='stock_amount', value=self.amount * 2
                    )  # 200 TRX
                    handler = this(dispatcher=self.dp)
                    self._get_result(handler=handler)  # делаем запрос
                    # вызвана для получения шаблона
                    answer_mock_function.assert_called_with(text=get_insufficient_funds_template())
                    # вызвана для получения клавиатуры
                    answer_mock_function.assert_called_with(text=get_insufficient_funds_template())
                    # проверяем, что ордера не тронуты, т.к. нет средств
                    self.assertEqual(self.offer.status, DirectExchangeOrder.Statuses.CREATED)
                    self.assertEqual(self.offer.already_exchanged_amount, Decimal('0'))
                    self.assertEqual(len(self.find()), 1)
                    # проверяем, что не было транзакций
                    mock_create_transaction_function.assert_not_called()

        # случай 2: достаточно средств, цена НЕ подходит (т.е. ниже той, по которой есть заявка)
        with patch(self.__get_balance_function(is_buy=True), return_value=Decimal('0.000240')):
            with patch(create_transaction_function_path) as mock_create_transaction_function:
                self.dp.machine.set_selected_values(
                    profile=self.profile, key='stock_amount', value=self.amount / 2
                )  # 50 TRX
                self.dp.machine.set_selected_values(
                    profile=self.profile, key='stock_price', value=self.offer.price - Decimal('0.00000001')
                )
                handler = this(dispatcher=self.dp)
                response_text = self._get_result_template(handler=handler)  # делаем запрос
                # проверим, что создался новый ордер, но старый не был тронут
                all_orders = self.find()
                self.assertEqual(len(all_orders), 2)
                self.assertEqual(self.offer.status, DirectExchangeOrder.Statuses.CREATED)
                self.assertEqual(self.offer.already_exchanged_amount, Decimal('0'))
                mock_create_transaction_function.assert_not_called()
                self.assertEqual('✅ Заявка на обмен #E3 успешно создана.', response_text)
                # почистим за собой
                all_orders[0].delete()

        # случай 3: достаточно средств, цена подходит, но заявка исполнена не полностью, т.к. не хватает предложений
        # добавим денег x2 больше чем в прошлом тесте, т.к. тут сумма увеличивается вдвое
        with patch(self.__get_balance_function(is_buy=True), return_value=Decimal('0.000500')):
            with patch(create_transaction_function_path) as mock_create_transaction_function:
                self.dp.machine.set_selected_values(
                    profile=self.profile, key='stock_amount', value=self.amount * 2
                )  # 200 TRX
                self.dp.machine.set_selected_values(profile=self.profile, key='stock_price', value=self.offer.price)
                handler = this(dispatcher=self.dp)
                response_text = self._get_result_template(handler=handler)  # делаем запрос
                self.offer.refresh_from_db()
                all_orders = self.find()
                # заявка, что была в стакане должна быть полностью исполнена
                self.assertEqual(len(all_orders), 1)
                self.assertEqual(self.offer.status, DirectExchangeOrder.Statuses.PROCESSED)
                # а остаться должна только вновь созданная заявка, исполненная частично
                new_order = all_orders[0]
                self.assertEqual(new_order.status, DirectExchangeOrder.Statuses.CREATED)
                # причем количество уже исполненной суммы должно быть одинаковым в обеих заявках
                self.assertEqual(new_order.already_exchanged_amount, self.offer.already_exchanged_amount)

        # случай 4: достаточно средств, но для исполнения по этой цене нужно несколько заявок
        # их всех хватает и даже еще остается
        # для этого формируем прототип заявки на продажу 20 TRX по цене 0.00000240 BTC
        prototype_order = __make_prototype_order(amount=20)
        # и копируем ее 9 раз
        __copy_prototype_order(prototype_order=prototype_order, copy_numbers=9)
        # проверяем, что у нас создалось ровно 9 заявок по 20 TRX + сам прототип
        self.assertEqual(len(self.find(limit=None)), 10)
        with patch(self.__get_balance_function(is_buy=True), return_value=Decimal('0.000500')):
            with patch(create_transaction_function_path) as mock_create_transaction_function:
                self.dp.machine.set_selected_values(
                    profile=self.profile, key='stock_amount', value=self.amount * Decimal('1.66')
                )  # 166 TRX
                self.dp.machine.set_selected_values(profile=self.profile, key='stock_price', value=self.offer.price)
                handler = this(dispatcher=self.dp)
                response_text = self._get_result_template(handler=handler)  # делаем запрос
                # в итоге 8 заявок по 20 TRX должны покрыть 160 TRX и уйти со стакана
                all_orders = self.find(limit=None)
                self.assertEqual(len(all_orders), 2)
                # а в ставкане должно остаться две заявки,
                # причем одна из них должна бытьчастично исполненая (6 TRX)
                self.assertEqual(all_orders[0].already_exchanged_amount, Decimal('6'))
                self.assertEqual(all_orders[1].already_exchanged_amount, Decimal('0'))

        # случай 5: достаточно средств, для исполнения по этой цене нужно несколько заявок
        # но их всех не хватает, в результате должна остаться одна заявка частично-исполненная
        all_orders = self.find(limit=None)
        all_orders[1].delete()
        prototype_order = __make_prototype_order(amount=20)
        __copy_prototype_order(prototype_order=prototype_order, copy_numbers=5)  # всего 100 TRX
        # проверяем, что у нас создалось ровно 5 заявок по 20 TRX + сам прототип
        self.assertEqual(len(self.find(limit=None)), 6)
        with patch(self.__get_balance_function(is_buy=True), return_value=Decimal('0.000500')):
            with patch(create_transaction_function_path) as mock_create_transaction_function:
                self.dp.machine.set_selected_values(
                    profile=self.profile, key='stock_amount', value=self.amount * Decimal('1.10')
                )  # 110 TRX
                self.dp.machine.set_selected_values(profile=self.profile, key='stock_price', value=self.offer.price)
                handler = this(dispatcher=self.dp)
                response_text = self._get_result_template(handler=handler)  # делаем запрос
                # в итоге 5 заявок по 20 TRX должны покрыть 100 TRX и уйти со стакана
                all_orders = self.find(limit=None)
                self.assertEqual(len(all_orders), 1)
                # ну а последняя должна быть частично-исполненая (10 TRX)
                self.assertEqual(all_orders[0].already_exchanged_amount, Decimal('10'))

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

    def test_direct_exchange(self):
        handler = DirectExchangeHandler(dispatcher=self.dp)
        result = handler.handle()
        self.assertIn(get_direct_exchange_main_template(), result['text'])
        expected = get_direct_exchange_main_keyboard()
        kb = result["reply_markup"].inline_keyboard
        self.assertEqual(expected[0][0][1], kb[0][0].callback_data)
        self.assertEqual(expected[1][0][1], kb[1][0].callback_data)
        self.assertEqual(expected[2][0][1], kb[2][0].callback_data)
        # проверим кнопку истории открытых ордеров
        self.create_order(
            profile=self.profile,
            crypto_from=self.crypto1,
            crypto_to=self.crypto2,
            amount=Decimal('1'),
            price=Decimal('1'),
            order_type='buy',
        )
        result = handler.handle()
        opened_orders_btn = result["reply_markup"].inline_keyboard[1][0]
        self.assertEqual('DirectExchangeHistoryHandler|created', opened_orders_btn.callback_data)
        self.assertEqual('						
Источник: https://habr.com/ru/articles/783250/


Интересные статьи

Интересные статьи

Приложения, созданные на платформе 1С:Предприятие, могут быть развернуты в трёхзвенной архитектуре (Клиент – Сервер приложений – СУБД). Клиентская часть приложения может работать, в частности, как веб...
Вчера ночью OpenAI выложил ещё несколько видео с OpenAI DevDAy и там довольно много интересного, как устроено то, что они анонсировали и более подробно, как они работают над новыми фичами. Видео дейст...
В этой статье я расскажу, как засунуть F# в Yandex Cloud Functions. Навыка работы с Serverless у меня нет, так что это будет не компиляция моего опыта, а отчет о вполне успешном эксперименте.Судя по в...
Немного магии, бережная работа с объектами/компонентами. В посте также рассмотрена ECS и то, почему в Factorio не так просто использовать такой подход.
Итак, моя первая публикация успешно прошла модерацию, поэтому рад вам представить вторую часть статьи, в которой мы применим полученные знания на практике и напишем прост...