Trading robot for web designers

    Writing trading robots is usually a rather time-consuming task - in addition to understanding the principles of trading (as well as understanding how this or that strategy looks), you need to know and be able to work with the protocols used for trading. In short - there are two main groups of protocols that are provided by the exchange or brokers: FIX , which cannot be figured out without a bottle, and a proprietary binary protocol, which is rarely better. This leads to one of two problems: either the code looks like any junior clutches his head, or a good, beautiful code that can do just about anything (and what it can do with different unexpected problems).



    In order to solve the above problems and attract as many participants as possible, brokers sometimes present the usual HTTP API with serialization in json / xml / something more exotic. In particular, this method of communicating with the exchange is almost the only one for a number of fashionable startups, for example, bitcoin exchanges. We decided to keep up with them and recently introduced an addition to our API (you can read more about its old features on the Habré here and here ), which allows the user to also trade.


    Under the cat, a not-so-Friday tutorial article on how to trade through our HTTP API.


    If you are a beginner, we suggest you read here .


    We will implement a robot that trades on a grid-strategy . It looks like this:


    1. Choose the price step (grid) stepand the amount of one order size.
    2. Save the current price.
    3. We get a new price and compare it with the saved one.
    4. If the price has changed less than by step, then return to step 3.
    5. If the price has changed more than by step, then:
      a. If the price has increased, then we place a request with a quantity sizefor sale.
      b. If decreased - then for a purchase with the same amount.
    6. Return to item 2.

    Clearly on the Bitcoin chart, the strategy is as follows:



    Instead of a programming language, we choose Python - because of the simplicity of working with some things and the speed of development. In the wake of hype for testing the robot, we take cryptocurrencies, say, lightcoins LTC.EXANTE(because there is no money for bitcoin).


    Login


    As before, you must have an account at https://developers.exante.eu (by the way, you can also log in via GitHub). The only difference from the old guides is that for trading we will need a trading account, to create which you need to log in to your personal account with a freshly created user.


    This time, to authorize the robot, there is no need to dance with a tambourine around jwt.io - the application will be launched on the developer's computer / server, so there is no need to insert additional security levels (and difficulties) in the form of tokens. Instead, we will use the usual http basic auth:



    The resulting Application ID is the username, and the Value column in the Access Keys is actually our password.


    Getting quotes


    Since the robot needs to know when and how to trade, we again need to get market data. To do this, we write a small class:


    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))
    

    I remind you of the need to encode the name of the tool, because it can contain, for example, a slash /( EUR/USD.E.FX). To actually receive the data, we write a generator method:


        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)

    Trading session adapter


    In order to trade, in addition to standard knowledge (financial instrument, order size and price, type of order), you need to know your account. To do this, unfortunately, you need to log in to your account and try our browser trading platform . Fortunately, in the future the API will be finalized - it will be possible to find out information about your user (including trading accounts) without leaving the cash desk. The account will be in the upper right corner, type 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()

    As you may have noticed, the prefix for placing orders and obtaining market data is different - /trade/1.0against /md/1.0. intervalhere it is used to indicate the interval between requests for data on requests from the server (I would not advise you to set it too small to avoid a ban):


        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()

    Read more about the fields in the answer here ; we will only be interested in the fields orderParameters.side, orderState.fills[].quantityand orderState.fills[].pricefor the calculationof losses profit.


    Method for placing an application for the server:


        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()

    This code section contains two new obscure phrases:


    • {'orderType': 'limit'}means that we put the so-called limit order so that the bad broker exchange does not heat us up on the market order, which (unlike the limit order) can be executed at an arbitrary reasonable (and sometimes not very) price.
    • {'duration': 'good_till_cancel'}means the lifetime of the order , in this case - until the trader gets bored (or something breaks).

    Watchdog for applications


    It will work in an infinite loop, and dump the results of the work in 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

    Strategy implementation


    As you may have noticed, we still haven’t reached the most interesting part, namely, the implementation of our trading strategy. It will look something like this:


    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

    Launch and debugging


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

    In the future, in order for the robot to be able to trade at all, we twist the parameter gridin accordance with the market fluctuation for the selected financial instrument. It should also be noted that this strategy is rarely used for anything other than forex. Nevertheless, our robot is ready.


    Known Issues


    • The robot is pretty dumb and can't do anything except trade on the same strategy with parameters fixed in advance ...
    • ... and can do it badly and fall with exceptions ...
    • ... and when it does not break, it will work slowly.
    • There is a problem with representing numbers in a type double. Here replacing doublewith Decimal.
    • There is no calculation of values ​​important to the trader, for example, PnL .

    Instead of a conclusion


    We tried to take into account a number of problems in our repository on GitHub devoted to this example. The code in the repository is sometimes documented and published under the MIT license. Below is also a short video demonstrating the work of our robot:



    Also popular now: