Написание торговых роботов, как правило, достаточно трудоемкая задача — помимо понимания принципов торговли (равно как и представления о том, как та или иная стратегия выглядит), необходимо знать и уметь работать с протоколами, используемыми для торговли. Вкратце — существуют две основные группы протоколов, которые предоставляются биржей или брокерами: 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. Ниже также представлено небольшое видео с демонстрацией работы нашего робота:


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


  1. EreminD
    06.10.2017 12:00
    +1

    Вместо языка программирования выберем Python

    смешно)


    1. s0ltan
      06.10.2017 15:45

      Вроде и так на питоне написано. Интересно, есть API для QUIK. И как для сбера. Хотя зная API, можно для любого докрутить.


  1. Voila2000
    07.10.2017 04:34

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


    1. arcan1s Автор
      07.10.2017 04:42

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

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

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


      1. SADKO
        07.10.2017 12:42

        Согласен, однако всё это такая мелочь в сравнении с реальными задачами, что в общем это дело наживное и\или делигируемое…
        Бизнес логика в общем-то тоже проста, и сколько я знаю людей все примерно делают одно и тоже, с поправками на реалии, кто-то сам с усам, кто то работает на фонд, а у кого-то самолёт не роскошь но средство передвижения…
        … но считать можно по разному, я например не разу не думал что буду вникать в теорию чисел, и использовать фортран, но тем не менее…


  1. SADKO
    07.10.2017 13:08

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

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


    1. arcan1s Автор
      07.10.2017 15:09

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


      1. SADKO
        07.10.2017 21:14

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

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

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


    1. Voila2000
      07.10.2017 15:57

      Правда жизни в том, что вэб дизайнер может получать деньги за свою работу с хорошим математическим ожиданием, которое на бирже ему не разу не светит!

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