Торговый робот для веб-дизайнеров

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



    Для того чтобы решить обозначенные выше проблемы и привлечь как можно больше участников, брокеры иногда представляют обычное HTTP API с сериализацией в json/xml/что-то более экзотическое. В частности, подобный метод общения с биржей является едва ли не единственным для ряда модных стартапов, например, биткоин-бирж. Мы решили не отставать от них и недавно представили дополнение к нашему API (подробнее про его старые возможности можно почитать на Хабре здесь и здесь), которое позволяет пользователю также и торговать.


    Под катом не совсем пятничная статья-туториал про то, как можно было бы торговать через наше HTTP API.


    Реализовывать мы будем робота, который торгует по grid-стратегии. Выглядит она следующим образом:


    1. Выберем шаг цены (сетки) step и количество одной заявки size.
    2. Сохраняем текущую цену.
    3. Получаем новую цену и сравним с сохраненной.
    4. Если цена изменилась меньше чем на step, то вернуться к п.3.
    5. Если цена изменилась больше чем на step, то:
      a. Если цена увеличилась, то ставим заявку с количеством size на продажу.
      b. Если уменьшилась — то на покупку с таким же количеством.
    6. Вернуться к п.2.

    Наглядно на графике биткоина стратегия выглядит следующим образом:



    Вместо языка программирования выберем Python — из-за простоты работы с некоторыми штуками и скорости разработки. На волне хайпа для тестирования робота возьмем криптовалюты, скажем, лайткоины LTC.EXANTE (потому что на биткоин денег нет).


    Авторизация


    Как и раньше, необходимо иметь аккаунт на https://developers.exante.eu (к слову, можно авторизоваться и через GitHub). Единственное отличие от старых гайдов — для торговли нам понадобится торговый аккаунт, для создания которого необходимо залогиниться в личный кабинет со свежесозданным пользователем.


    В этот раз для авторизации робота нет необходимости танцевать с бубном вокруг jwt.io — приложение будет запущено на компьютере/сервере разработчика, поэтому нет необходимости вставлять дополнительные уровни безопасности (и трудности) в виде токенов. Вместо это мы будем использовать обычный http basic auth:



    Полученные Application ID — имя пользователя, а колонка Value в Access Keys – собственно наш пароль.


    Получение котировок


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


    class FeedAdapter(threading.Thread):
        def __init__(self, instrument: str, auth: requests.auth.HTTPBasicAuth):
            super(FeedAdapter, self).__init__()
            self.daemon = True
    
            self.__auth = auth
            self.__stream_url = 'https://api-demo.exante.eu/md/1.0/feed/{}'.format(
                urllib.parse.quote_plus(instrument))
    

    Я напомню о необходимости кодирования имени инструмента, потому что оно может содержать, например, слэш / (EUR/USD.E.FX). Для собственно получения данных напишем метод-генератор:


        def __get_stream(self) -> iter:
            response = requests.get(
                self.__stream_url, auth=self.__auth, stream=True, timeout=60,
                headers={'accept': 'application/x-json-stream'})
            return response.iter_lines(chunk_size=1)
    
        def run(self) -> iter:
            while True:
                try:
                    for item in self.__get_stream():
                        # парсим ответ сервера
                        data = json.loads(item.decode('utf8'))
                        # к сожалению, API на текущий момент имеет несколько 
                        # различный набор полей для ответа. Наличие поля event 
                        # означает служебное сообщение, иначе - цены в с полями 
                        # {timestamp, symbolId, bid, ask}
                        if 'event' in data:
                            continue
                        # а вот и наши котировки
                        yield data
                # обработка стандартных ошибок
                except requests.exceptions.Timeout:
                    print('Timeout reached')
                except requests.exceptions.ChunkedEncodingError:
                    print('Chunk read failed')
                except requests.ConnectionError:
                    print('Connection error')
                except socket.error:
                    print('Socket error')
                time.sleep(60)

    Адаптер к торговой сессии


    Для того чтобы торговать, помимо стандартных знаний (финансовый инструмент, размер и цена заявки, тип заявки), нужно знать свой аккаунт. Для этого, к сожалению, необходимо авторизоваться в личном кабинете и попробовать нашу браузерную торговую платформу. К счастью, в будущем API будет доработано — появится возможность узнать информацию о своем пользователе (включая торговые аккаунты) не отходя от кассы. Аккаунт будет в верхнем правом углу, вида ABC1234.001:



    class BrokerAdapter(threading.Thread):
        def __init__(self, account: str, interval: int, auth: requests.auth.HTTPBasicAuth):
            super(BrokerAdapter, self).__init__()
            self.__lock = threading.Lock()
            self.daemon = True
            self.__interval = interval
    
            self.__url = 'https://api-demo.exante.eu/trade/1.0/orders'
    
            self.__account = account
            self.__auth = auth
            # внутреннее хранилище заявок для проверки их состояния
            self.__orders = dict()

    Как вы могли заметить, префикс для постановки заявок и получения рыночных данных отличается — /trade/1.0 против /md/1.0. interval здесь служит для указания интервала между запросами данных по заявкам с сервера (не советовал бы ставить слишком маленький во избежание бана):


        def order(self, order_id: str) -> dict:
            response = requests.get(self.__url + '/' + order_id, auth=self.__auth)
            if response.ok:
                return response.json()
            return dict()

    Подробнее о полях в ответе можно почитать здесь; нас же будут интересовать только поля orderParameters.side, orderState.fills[].quantity и orderState.fills[].price для расчета потерь профита.


    Метод для постановки заявки на сервер:


        def place_limit(self, instrument: str, side: str, quantity: int,
                        price: float, duration: str='good_till_cancel') -> dict:
            response = requests.post(self.__url, json={
                'account': self.__account,
                'duration': duration,
                'instrument': instrument,
                'orderType': 'limit',
                'quantity': quantity,
                'limitPrice': price,
                'side': side
            }, auth=self.__auth)
            try:
                # заявка поставлена, нас интересует только ее ID
                return response.json()['id']
            except KeyError:
                # ответ сервера содержит какую-то читаемую ошибку
                print('Could not place order')
                return response.json()
            except Exception:
                # все сломалось, время выводить свои деньги
                print('Unexpected error occurs while placing order')
                return dict()

    Данный участок кода содержит два новых непонятных словосочетания:


    • {'orderType': 'limit'} означает, что мы ставим так называемую лимитную заявку, чтобы плохие брокер-биржа не нагрели нас на маркетной заявке, которая (в отличие от лимитной) может исполниться по произвольной разумной (а иногда и не очень) цене.
    • {'duration': 'good_till_cancel'} означает время жизни заявки, в данном случае — пока трейдеру не надоест (или что-то не сломается).

    Watchdog для заявок


    Работать он будет в бесконечном цикле, а результаты работы сваливать в stdout:


        def run(self) -> None:
            while True:
                with self.__lock:
                    for order_id in self.__orders:
                        state = self.order(order_id)
                        # проверить, изменилось ли состояние заявки
                        if state == self.__orders[order_id]:
                            continue
                        print('Order {} state was changed'.format(order_id))
                        self.__orders[order_id] = state
                        # давайте посчитаем наши филы, если они были
                        filled = sum(
                            fill['quantity'] for fill in state['orderState']['fills']
                        )
                        avg_price = sum(
                            fill['price'] for fill in state['orderState']['fills']
                        ) / filled
                        print(
                            'Order {} with side {} has price {} (filled {})'.format(
                            order_id, state['orderParameters']['side'], avg_price, 
                            filled
                        ))
                # ждать до следующей проверки
                time.sleep(self.__interval)
    
        # добавить/удалить заявку из watchdog
        def add_order(self, order_id: str) -> None:
            with self.__lock:
                if order_id in self.__orders:
                    return
                self.__orders[order_id] = dict()
    
        def remove_order(self, order_id: str) -> None:
            with self.__lock:
                try:
                    del self.__orders[order_id]
                except KeyError:
                    pass

    Реализация стратегии


    Как вы могли заметить, мы так и не дошли до самого интересного, а именно до реализации нашей торговой стратегии. Выглядеть она будет примерно так:


    class GridBrokerWorker(object):
        def __init__(self, account: str, interval: str, application: str, token: str):
            self.__account = account
            self.__interval = interval
            # объект с авторизацией
            self.__auth = requests.auth.HTTPBasicAuth(application, token)
    
            # создадим брокер-адаптер и сразу его запустим
            self.__broker = broker_adapter.BrokerAdapter(
                self.__account, self.__interval, self.__auth)
            self.__broker.start()
    
        def run(self, instrument, quantity, grid) -> None:
            # здесь мы создадим адаптер для фида и подпишемся на его обновления
            feed = feed_adapter.FeedAdapter(instrument, self.__auth)
            old_mid = None
            for quote in feed.run():
                mid = (quote['bid'] + quote['ask']) / 2
                # если это первая котировка, то не делаем ничего
                if old_mid is None:
                    old_mid = mid
                    continue
                # если не первая, то прищуриваемся и проверяем не больше ли изменение
                # цены, чем шаг
                if abs(old_mid - mid) < grid:
                    continue
                # проставляем цену в зависимости от того, в какую сторону изменилась цена
                side = ‘sell’ if mid - old_mid > 0 else ‘buy’
                # ставим заявку
                order_id = self.__broker.place_limit(
                    instrument, side, str(quantity), str(mid))
    
                # обрабатываем результат
                if not order_id:
                    print('Unexpected error')
                    continue
                # читаемая ошибка
                elif not isinstance(order_id, str):
                    print('Unexpected error: {}'.format(order_id))
                    continue
                # заявка поставилась! Добавляем ее к watchdog...
                self.__broker.add_order(order_id)
                # ...и обновляем уровень цены
                old_mid = mid

    Запуск и отладка


    # создадим экземпляр класса
    worker = GridBrokerWorker('ABC1234.001', 60, 'appid', 'token')
    # запустим
    worker.run('LTC.EXANTE', 100, 0.1)

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


    Известные проблемы


    • Робот довольно тупой и не умеет ничего делать, кроме как торговать по одной стратегии с фиксированными заранее параметрами...
    • … и может делать это плохо и падать с исключениями...
    • … а когда не сломается, будет работать неспешно.
    • Есть проблема с представлением чисел в типе double. Тут поможет замена double на Decimal.
    • Нет расчета величин важных для трейдера, например, PnL.

    Вместо заключения


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


    EXANTE 52,49
    Инвестиционная компания нового поколения
    Поделиться публикацией
    Комментарии 9
    • +1
      Вместо языка программирования выберем Python

      смешно)
      • 0
        Вроде и так на питоне написано. Интересно, есть API для QUIK. И как для сбера. Хотя зная API, можно для любого докрутить.
      • 0
        Вообще, если торговать на бирже, например на ММВБ через QUIK, плазу или еще какую платформу, то там все API описаны, задокументированны и снабжены работающими примерами на популярных языках программирования. Есть еще и сторонние библиотеки типа бесплатного StockSharp, в котором есть и FIX, и куча других коннекторов к зарубежным площадкам.
        И все это добро, при грамотном использовании, вполне стабильно работает. С надежным исполнением ордеров и контролем остатков.
        Мне кажется, что основная трудоемкость написания робота, это постоянный поиск рабочей идеи, той бизнес логики, которая приносит прибыль, с учетом стоимости накладных расходов, при заданном уровне риска. А «работа с протоколами», это просто часть инфраструктуры, которую можно получить в готовом виде у брокера или у разработчика торговой платформы.
        • 0
          как разработчик адаптера к плазе, я бы не сказал, что примеры «хорошие», а документация в части конфигурации прямо таки скажем несколько хромает. Проблема в том, что примеры — как и здесь — нечто минимально работающее, что ирл не является достаточным.

          К слову, фикс, на самом деле, довольно простой — там необходимым и, едва ли, не достаточным для работы является наличие валидного FIX**.xml и допиленного quickfix для выбранного языка (хотя первая реакция, как правило, «щито?!»).

          PS со StockSharp не работал, у нас в компании c# используется только для внутренних тулз. Но по личному опыту, для каждого конкретного коннекта существует куча ньюансов, которые нужно учесть, что сводит на нет полезность такого рода библиотек при попытке подключения к не очень популярному агенту.
          • 0
            Согласен, однако всё это такая мелочь в сравнении с реальными задачами, что в общем это дело наживное и\или делигируемое…
            Бизнес логика в общем-то тоже проста, и сколько я знаю людей все примерно делают одно и тоже, с поправками на реалии, кто-то сам с усам, кто то работает на фонд, а у кого-то самолёт не роскошь но средство передвижения…
            … но считать можно по разному, я например не разу не думал что буду вникать в теорию чисел, и использовать фортран, но тем не менее…
        • 0
          Ребята, ну вы вообще, сеточник на питоне для вэб дизайнеров.
          Вам лавры Метаквотов не дают покоя? Так у них в пятом метаке, есть годный конструктор ботов для лохов, куча модулей к нему, а так-же система тестирования и оптимизации с генетикой или без…

          Тут надо объяснить, что бы люди понимали, им впаривают идею «заработай на бирже, тыж программист» :-) Раньше было «вот тебе бесплатные\дорогие курсы тех анализа, ты-ж умный, что.» Совсем дебильные варианты, вроде «наши аналитики будут вам подсказывать» мы не рассматриваем.
          Правда жизни в том, что вэб дизайнер может получать деньги за свою работу с хорошим математическим ожиданием, которое на бирже ему не разу не светит!
          Знание шахматных фигур и правил игры никак не поможет им в игре против гроссмейстера, а здесь не шахматы, а покер в дорогом казино, с профессиональными игроками и каталами за столом.
          • 0

            Вы слишком серьезны. Рассмотренный здесь пример не более чем занятая игрушка, я думал стиль повествования это подразумевает (я намерено не акцентировал на финансовой части, например).

            • 0
              Ну правильно, что бы поиграться, убедиться что работает, як-як и в продакшен :-)
              Чего брокеру и надо, это-же лафа — тупой сеточник, а если к нему мартын прикрутить, будет вообще сказка, зря мартын не добавили, это может быть темой следующего поста, серьёзно.
              И картиночки с доходностью, не забудьте или…

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

              Так-что всё у вас правильно, и всё это уже было, например один весёлый банк, даёт клиентам годных торговых роботов, реально годных, что бы лохи прониклись и сами собирали деньги на депо, и робот торговал бы большим объёмом. У клиента складывается впечатление что он что-то в этой жизни понял, но понимания того что рынок живой, и будет реагировать на возросшие объёмы как-то иначе, ему и в голову не приходит…
            • 0
              Правда жизни в том, что вэб дизайнер может получать деньги за свою работу с хорошим математическим ожиданием, которое на бирже ему не разу не светит!

              Так и есть, в самую суть. Очень понравилось как сформулировали. На некоторых интернет-площадках, так вообще не казино, а наперсточники ))

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

            Самое читаемое