📜 ⬆️ ⬇️

Trading robot for web designers

Writing trading robots, as a rule, is quite a laborious task - in addition to understanding the principles of trading (as well as ideas about 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 understood without a bottle, and the proprietary binary protocol, which is rarely better. This leads to one of two problems: either the code looks like any junior grabs his head, or a good, beautiful code that can do about nothing (and does what he can do with various unexpected problems).



In order to solve the problems outlined above and attract as many participants as possible, brokers sometimes present the usual HTTP API with serialization in json / xml / something more exotic. In particular, such a 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 submitted an add-on to our API (you can read more about its old features in Habré here and here ), which allows the user to trade as well.


Under the cut is not quite Friday article-tutorial about how you could trade through our HTTP API.


If you are a novice trader, then we suggest you read here .


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


  1. Select the price step (grid) step and the number of one order size .
  2. Save the current price.
  3. We get a new price and compare with the saved.
  4. If the price has changed by less than step , then return to p.3.
  5. If the price has changed by more than step , then:
    a. If the price has increased, then we put an order with the amount of size for sale.
    b. If it has decreased, then for a purchase with the same amount.
  6. Return to p.2.

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



Instead of a programming language, we’ll choose Python because of the simplicity of working with some pieces and the speed of development. In the wake of the HYIP, for testing the robot, let's take cryptocurrencies, say, LTC.EXANTE LTC.EXANTE (because there is no money for LTC.EXANTE ).


Authorization


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


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



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


Get quotes


Since the robot needs to know when and how to trade, we again need to obtain 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 will remind you of the need to encode the name of the instrument, because it may contain, for example, a slash / ( EUR/USD.E.FX ). For the actual data acquisition, we will 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) 

Adapter to the trading session


In order to trade, in addition to standard knowledge (financial instrument, size and price of the application, type of application), you need to know your account. To do this, unfortunately, you need to log in to your account and try our browser-based trading platform . Fortunately, in the future, the API will be refined - it will be possible to find out information about your user (including trading accounts) without departing from the cash register. 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 can see, the prefix for placing orders and receiving market data is different - /trade/1.0 against /md/1.0 . interval here is used to specify the interval between requests for data on requests from the server (I would not advise you to set 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() 

You can read more about the fields in the answer here ; we are interested only in the fields orderParameters.side , orderState.fills[].quantity and orderState.fills[].price for calculation losses profit


Method for making an application to 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 section of code contains two new incomprehensible phrases:



Watchdog for applications


It will work in an infinite loop, and dump the results of the work into 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 can see, we have not reached the most interesting, namely, the implementation of our trading strategy. It will look 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 

Run and debug


 #    worker = GridBrokerWorker('ABC1234.001', 60, 'appid', 'token') #  worker.run('LTC.EXANTE', 100, 0.1) 

Further, in order for the robot to trade at all, we twist the grid parameter in accordance with market fluctuations 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



Instead of conclusion


We tried to take a number of issues into our GitHub repository , dedicated to this example. The code in the repository is documented and published under the MIT license. Below is also a small video demonstrating the work of our robot:



')

Source: https://habr.com/ru/post/339446/


All Articles