Building a Mean Reversion Strategy with the lemon.markets API & hosting it in the cloud


blog photo
Published by Marius Sprenger on September 7, 2021
Trading


Hi there. My name is Marius and I am our community developer at lemon.markets 🍋. We are building an API that allows you to build your own brokerage experience at the stock market. This may include, among other possibilities, your very own automated trading strategy. We are working on our sandbox and are launching it soon, but maybe you find your way in already. Otherwise, feel free to sign up to the waitlist! To inspire you a little bit for your first project with us: Might I suggest a starting point?

In this post, I want to dive into how you can use the lemon.markets API to build one of the most well-known trading strategies: the mean reversion strategy. And, to set you up for success, I will also walk you through the steps to host your strategy up in the cloud using Heroku, to make sure your program continues to run, even when your laptop is closed.

The premise of the mean-reversion strategy (I am going to call it MR strategy from now on because who likes words that fill up entire lines?) is easily explained: behind it stands the assumption that a stock will eventually converge towards a mean (or “average”) value. So imagine you have a stock from the up-and-coming online supermarket “SuperCompany” that has a mean share price of €97.00 (e.g. over the past two weeks) and is currently traded at €92.50. The MR strategy assumes that the price will soon converge towards the average share price, meaning that now would be a good time to buy (#buythedip, anyone?). On the other hand, if the current share price was €103.50 right now, it would be (if we follow the MR logic) a good idea to sell the stock, as it is currently higher than the average share price over the past few weeks and we therefore expect it to decrease soon.

Sounds simple, right? It is.

And more importantly, the logic behind it is easy to implement if we have the right infrastructure in place. And, I’m sure you guessed it: that’s where lemon.markets comes into play. Using the lemon.markets API, you can build a MR strategy on your own with very little effort. Word has it that Jim Simons’ early successes can be attributed to a MR-based strategy. For anyone interested in diving deeper inside the mean reversion strategy, we can recommend this article.

But for now, let’s dive right in.

Implementing the Mean Reversion Strategy

In order to keep an overview of our Python project, we first define a number of helper files and functions that we can later use in our main script. For that, we create a file helper.py and implement the class RequestHandler in there, which contains a number of helper functions to make API requests, which we can then subsequently reuse and therefore do not need to write each request separately. We can use the functions to request a new token and to make GET, PUT and POST requests.

1import os
2import requests
3from dotenv import load_dotenv
4class RequestHandler:
5    load_dotenv()
6    url_data: str = os.environ.get("BASE_URL_DATA")
7    url_trading: str = os.environ.get("BASE_URL_TRADING")
8    auth_url: str = os.environ.get("AUTH_URL")
9    def get_token(self, endpoint: str, data):
10        """
11        :param data:
12        :param endpoint:
13        :return: access token as String
14        """
15        response = requests.post(self.auth_url + endpoint, data)
16        return response
17    def get_data_trading(self, endpoint: str):
18        response = requests.get(self.url_trading + endpoint,
19                                headers={
20                                    "Authorization": "Bearer " + os.environ.get("TOKEN_KEY")
21                                })
22        return response.json()
23    def get_data_data(self, endpoint: str):
24        """
25        :param endpoint: {str} only append the endpoint to the base url
26        :return:
27        """
28        response = requests.get(self.url_data + endpoint,
29                                headers={
30                                    "Authorization": "Bearer " + os.environ.get("TOKEN_KEY")
31                                })
32        return response.json()
33    def put_data(self, endpoint: str):
34        response = requests.put(self.url_trading + endpoint,
35                                headers={
36                                    "Authorization": "Bearer " + os.environ.get("TOKEN_KEY")
37                                })
38        return response.json()
39    def post_data(self, endpoint: str, data):
40        response = requests.post(self.url_trading + endpoint,
41                                 data,
42                                 headers={
43                                     "Authorization": "Bearer " + os.environ.get("TOKEN_KEY")
44                                 })
45        return response.json()

Environment Variables

If you look at the file above, you will notice that we access a number of environment variables. If you want to run your script locally, we suggest that you create a .env file containing the following variables, which can then be accessed via

1os.getenv(“ENV-VARIABLE")
1| ENV Variable   |      Explanation      |  
2|----------|:-------------:|
3| TOKEN_KEY |  Your Access Token | 
4| CLIENT_ID |   Your client id   |   
5| CLIENT_SECRET | Your client secret |
6|MIC| Market Identifier Code of Trading Venue|
7|BASE_URL_TRADING | Base URL of our paper money API |
8|BASE_URL_DATA | Base URL of our paper money API |
9|AUTH_URL | URL of our authentication API|
10|SPACE_UUID | Your Space UUID |

Environment Variables for Mean Reversion Python script

If you want to host your algorithm in the cloud, you need to follow a different approach, but we will come to that later.

Defining a number of models

In order to keep an overview within our project, we define a number of models as classes that we can then use throughout the project. These models directly reflect our API structure, which you can learn more about in ourdocumentation. Thereby, we define functions that are directly related to the respective class.

Order class

In the end, we want to place either a buy or a sell order, depending on what our mean reversion criteria suggest, which is why we create an Order class and assign a number of functions to it. In the end, we want to be able to do three different things in the context of orders:

  • placing an order
  • activating an order
  • seeing all of our orders

All of the three things above can be realised through API calls, which is why we make the Order class a sub-class of our RequestHandler class we defined in our helper.py file earlier. Thus, we can then access the GET, PUT and POST requests in our Order class.

1import os
2from dotenv import load_dotenv
3from helpers import RequestHandler
4class Order(RequestHandler):
5    def __init__(self, isin: str = "", valid_until: float = 0, quantity: int = 0, side: str = "",
6                 stop_price: float = 0, limit_price: float = 0, uuid: str = ""):
7        self.isin = isin
8        self.valid_until = valid_until
9        self.quantity = quantity
10        self.side = side
11        self.stop_price = stop_price
12        self.limit_price = limit_price
13        self.uuid = uuid
14    def place_order(self):
15        order_details = {
16            "isin": self.isin,  # set ISIN
17            "valid_until": self.valid_until,  # specify your timestamp
18            "side": self.side,  # set side
19            "quantity": self.quantity,  # set quantity
20        }
21        load_dotenv()
22        space_uuid = os.getenv("SPACE_UUID")
23        endpoint = f'spaces/{space_uuid}/orders/'
24        response = self.post_data(endpoint, order_details)
25        return response
26    def get_orders(self):
27        load_dotenv()
28        space_uuid = os.getenv("SPACE_UUID")
29        endpoint = f'spaces/{space_uuid}/orders/'
30        response = self.get_data_trading(endpoint)
31        return response
32    def activate_order(self, order_uuid):
33        load_dotenv()
34        space_uuid = os.getenv("SPACE_UUID")
35        endpoint = f'spaces/{space_uuid}/orders/{order_uuid}/activate/'
36        response = self.put_data(endpoint)
37        return response

Instruments class

We follow a similar approach with our Instruments class. Here, we define one function to get historical OHLC market data and one to get the latest OHLC data. We will see in a second where exactly in our Mean Reversion strategy we need that information.

1import os
2from dotenv import load_dotenv
3from helpers import RequestHandler
4import time
5class Instruments(RequestHandler):
6    def __init__(self, isin: str = "", x1: str = ""):
7        self.isin = isin
8        self.x1 = x1
9    def get_market_data(self):
10        load_dotenv()
11        mic = os.getenv("MIC")
12        from_date = time.time() - (86400 * 7)
13        to_date = time.time()
14        isin = self.isin
15        x1 = self.x1
16        endpoint = f'ohlc/{x1}/?mic={mic}&isin={isin}&from={from_date}&to={to_date}'
17        response = self.get_data_data(endpoint)
18        print(response)
19        return response
20    def get_latest_market_data(self):
21        load_dotenv()
22        mic = os.getenv("MIC")
23        isin = self.isin
24        x1 = self.x1
25        endpoint = f'ohlc/{x1}/?mic={mic}&isin={isin}&from=latest'
26        try:
27            response = self.get_data_data(endpoint)
28            close_price = response['results'][0].get('c', None)
29            return close_price
30        except Exception as e:
31            print(e)

TradingVenue class

One of the important principles when using our API, is the possibility to address specific trading venues. Therefore, we create a separate model for Trading Venues. We need three functions in there, which:

  • check if a trading venue is currently open
  • check the general opening times for the next days
  • determine the seconds until the trading venue reopens.
1import os
2from dotenv import load_dotenv
3from helpers import RequestHandler
4from datetime import datetime, timedelta
5import datetime
6class TradingVenue(RequestHandler):
7    def __init__(self, is_open: bool = False):
8        self.is_open = is_open
9    def check_if_open(self):
10        load_dotenv()
11        mic = os.getenv("MIC")
12        endpoint = f'venues/?mic={mic}'
13        response = self.get_data_data(endpoint)
14        self.is_open = response['results'][0].get('is_open', None)
15        return self.is_open
16    def get_opening_times(self):
17        load_dotenv()
18        mic = os.getenv("MIC")
19        endpoint = f'venues/?mic={mic}'
20        response = self.get_data_data(endpoint)
21        return response
22    def seconds_till_tv_opens(self):
23        times_venue = self.get_opening_times()
24        today = datetime.datetime.today()
25        opening_days_venue = times_venue['results'][0].get('opening_days', None)
26        next_opening_day = datetime.datetime.strptime(opening_days_venue[0], '%Y-%m-%d')
27        next_opening_hour = datetime.datetime.strptime(times_venue['results'][0]['opening_hours'].get('start', None),                                            '%H:%M')
28        date_difference = next_opening_day - today
29        days = date_difference.days + 1
30        if not self.check_if_open():
31            print('Trading Venue not open')
32            time_delta = datetime.datetime.combine(
33                datetime.datetime.now().date() + timedelta(days=1), next_opening_hour.time()
34            ) - datetime.datetime.now()
35            print(time_delta.seconds + (days * 86400))
36            return time_delta.seconds
37        else:
38            print('Trading Venue is open')
39            return 0

Token class

You can make requests against our API by using an access token, which you can get by making a PUT request against our authentication API using your client ID and client secret from your respective space. However, our access tokens have an expiration date. To prevent a scenario, where we try to use an access token that has expired, we create a Token class and attach a function that requests a new token.

1import os
2from helpers import RequestHandler
3class Token(RequestHandler):
4    def get_new_token(self):
5        token_details = {
6            "client_id": os.getenv("CLIENT_ID"),
7            "client_secret": os.getenv("CLIENT_SECRET"),
8            "grant_type": "client_credentials",
9        }
10        endpoint = f'token/'
11        response = self.get_token(endpoint, token_details)
12        os.environ['TOKEN_KEY'] = response.json().get('access_token', None)
13        return os.getenv('TOKEN_KEY')

Defining our main script

After we created all helper functions and models, we can start writing the general mean reversion logic in our main script. At first, we define a function mean_reversion_decision, which contains our decision logic. First, we get the historical market data for an instrument of our choice and calculate the average price for it. This is component 1 for our mean reversion decision.

Next, we get the latest close price, which then serves as our second decision variable. We then compare the average with the latest price and return True or False, depending on which one is higher (if the latest price is lower than the average price, we assume that it will eventually converge towards the mean, which is why we return True in that case).

Next, we want to use that information in our subsequent steps. Depending on what our mean_reversion_decision function returns, we either buy or sell a stock. As soon as the order is executed, we let our script sleep for 4 hours until we check again (once again, feel free to check more/less frequently here. This script is only a first starting point and there are many aspects that can be tweaked and refined).

Finally, we define our main function mean_reversion(). Here, we check if our Trading Venue is currently open and, after making sure that we have a functioning access token, execute our mean reversion logic. If the Trading Venue is closed, we pause our script until it reopens.

1from models.Order import Order
2from models.Instruments import Instruments
3from models.TradingVenue import TradingVenue
4from models.Token import Token
5import time
6import statistics
7def mean_reversion_decision(isin: str, x1: str = "d1"):
8    """
9    :param isin: pass the isin of your instrument
10    :param x1: pass what type of data you want to retrieve (m1, h1 or d1)
11    :return: returns whether you should buy (True) or sell (False), depending on MR criteria
12    """
13    market_data = Instruments(
14        isin=isin,
15        x1=x1
16    ).get_market_data()
17    d1_prices = market_data['results']
18    prices_close = [x["c"] for x in d1_prices]  # you can obviously change that to low, close or open
19    mean_price = statistics.mean(prices_close)
20    print(f'Mean Price: {mean_price}')
21    latest_close_price = Instruments(
22        isin=isin,
23        x1="m1"
24    ).get_latest_market_data()
25    print(f'Latest Close Price: {latest_close_price}')
26    if latest_close_price < mean_price:
27        return True
28    return False
29def check_if_buy(isin: str, x1: str = "d1"):
30    """
31    :param isin: pass the isin of the stock you are interested in
32    :param x1:  pass the market data format you are interested in (m1, h1, or d1)
33    """
34    # check for MR decision
35    if mean_reversion_decision(
36            isin=isin,
37            x1=x1
38    ):
39        # create a buy order if True is returned by MR decision function
40        try:
41            print('buy')
42            placed_order = Order(
43                isin=isin,
44                valid_until=time.time() + 86400,
45                side="buy",
46                quantity=1
47            ).place_order()
48            order_uuid = placed_order.get('uuid')
49            # subsequently activate the order
50            activated_order = Order().activate_order(order_uuid)
51            print(activated_order)
52            time.sleep(14400)  # check back in 4 hours
53        except Exception as e:
54            print(f'1{e}')
55            time.sleep(60)
56    else:
57        try:
58            # create a sell order if mean reversion decision returns False
59            print('sell')
60            placed_order = Order(
61                isin=isin,
62                valid_until=time.time() + 86400,
63                side="sell",
64                quantity=1
65            ).place_order()
66            order_uuid = placed_order.get('uuid')
67            activated_order = Order().activate_order(order_uuid)
68            print(activated_order)
69            time.sleep(14400)  # check back in 4 hours
70        except Exception as e:
71            print(f'2{e}')
72            time.sleep(60)
73def mean_reversion():
74    """
75    main function to be executed
76    """
77    while True:
78        if TradingVenue().check_if_open():
79            # make sure that you always have a functioning token
80            Token().get_new_token()
81            # make buy or sell decision
82            check_if_buy(
83                isin="US88160R1014",  # this is Tesla, but you can obviously use any ISIN you like :)
84                x1="d1"
85            )
86        else:
87            # sleep until market reopens in case it is closed
88            time.sleep(TradingVenue().seconds_till_tv_opens())
89if __name__ == '__main__':
90    mean_reversion()

And that was basically it. You can find the whole repo onGitHub— feel free to test it out. Obviously, we’d be super grateful if you decide to contribute by opening a PR or sending us a message to[email protected]. Looking forward to your comments/improvements 🙂.

Hosting in the Cloud

Obviously, an important part of a trading algorithm is to make sure that is constantly running. There are a number of possibilities to host your Python (or any other) script in the cloud. We decided to go with Heroku, as it is extremely convenient and fast to set up.

To host a Python script in the cloud, go to your dashboard and create a new application. After this step is done, go to the application’s “Deploy” tab and connect to your GitHub repository, where the script is hosted. Choose automatic deploys if you want to trigger a new deploy every time you make changes to the repository, or manual deploy if you wish to have a bit more “manual control” over your Heroku deploys.

For those of you who read this blog post carefully, you will have noticed that we still need to configure our environment variables. You can do so by either entering them in the dashboard under Settings/Config Vars or by logging into the Heroku CLI and setting them using:

1heroku config:set ENV_VAR=value

Afterwards, you can deploy your application. It might take a few minutes until your app is up and running. You can check the status by typing

1heroku logs

in your terminal/console). If the app is not running, try typing:

1heroku ps:scale worker=1

Take a look at this and this article if you should get stuck at some point during the Heroku setup.

If everything is up and running, you can relax, sit back, order some pizza and let the algorithm do the work for you 😎. Because at the end, this is what lemon.markets is there for: automating your trades so you have more time for other things.

Additional Considerations

There are a few additional decisions you need to make when building your own MR strategy. For example, how to determine the mean? Will you look at a time frame of two weeks, like we did? Or, will you look at hourly data? In addition, you do not need to restrict MR to prices, you can also look, for example, at a stock’s price-to-earnings (P/E) ratio. In our code snippet, we make trades if they are absolutely bigger or smaller than the mean, but perhaps you have reason to build your strategy such that orders are only placed if the current price is >0.05% larger than the mean.

As you can see, there is more than one way to make this base strategy more complex (and hopefully sophisticated).

Mean Reversion or Momentum?

You might have noticed that the MR strategy is built upon a critical assumption, namely that any extreme price movement will be followed by a return to long-run averages. But, is this a reasonable assumption to make? After all, a dramatic drop in instrument price might be persistent, for example, if it turns out that a stock loses its relevance in the market.

The MR strategy is based on regression to the mean, a statistical concept that describes the phenomenon that an unlikely event is usually followed by an expected event, rather than a more unlikely one. What’s the catch? This concept works only for systems with a normal distribution, which the stock market isn’t. Instead, stock returns more closely resemble a distribution with fat tails. In other words, in the stock market, extreme events happen more frequently than expected.

Why is MR so widely used, then? Many traders believe that the stock market oscillates between periods of mean reversion and momentum. In simple English, when an instrument price is increasing, it continues to increase (and the same can be said for downwards movements). (By the way: periods of momentum are why we see fat tails!) Mean reversion might not reflect the marketalways, but it does sometimes. So, how can you know whether we’re in a period of mean reversion or momentum? That’s the big question 😉 There are a few measures that can be applied to historical price data, such as the Hurst exponent. You can use this scalar to characterise a time series as mean-reverting, random-walk or trending. Based on that, you might want to decide whether to employ a mean reversion or trend following strategy. If you are interested in this sort of stuff, you can continue reading here.

I hope you got an idea of what you can do with the lemon.markets API. Let us know what you think, tell us about a bug you found or share a strategy that you built. Simply hit us up at [email protected] or join our Slack community. We’d love to hear from you 🍋 💛 .

Marius

You might also be interested in

Setting up your own Telegram bot to trade with the lemon.markets API (Part 1 of 2)

blog photo

Hi! My name is Joanne and I’m part of the team at lemon.markets. I’ve been working on a fun use-case for our product for the past few weeks and I’m very excited to share it with you! There’s hundreds of use-cases for our product, from automated trading strategies to portfolio visualisation dashboards. Today, I’ll show you how you can connect the lemon.markets API to the Telegram API. Why? So you can have a personalised butler — ahem, bot — that can place trades with a few very simple messages.

Creating your own lemon.markets Dashboard using Flutter

blog photo

Hey there. My name is Marius, and I am part of lemon.markets, an early-stage startup from the heart of Berlin that is working on an infrastructure that lets you build your own brokerage experience at the stock market. In this blog post, I will walk you through a project developed by our community member Melanie, where we will tackle the basics of building your own mobile-optimised Trading dashboard using Flutter. Does that sound like fun to you? Then let’s not waste any more time and get going.

Building a Mean Reversion Strategy with the lemon.markets API & hosting it in the cloud

blog photo

In this post, I want to diveinto how you can use the lemon.markets API to build one of the most well-known trading strategies: the Mean reversion Strategy. And, to set you up for success, I will also walk you through the steps to host your strategy up in the cloud using Heroku, to make sure your program continues to run, even when your laptop is closed.

Dive Deeper

Find more resources to get started easily

Check out our documentation to find out more about our API structure, different endpoints and specific use cases.

Engage

Join lemon.markets community

Join our Slack channel to actively participate in our community, ask questions to other users and stay up to date at all times.

Contribute

Interested in building lemon.markets with us?

We are always looking for great additions to our team that help us build a brokerage infrastructure for the 21st century.

Products
Pricing
For Developers
SlackGithubBlog
© lemon.markets 2021Privacy PolicyImprint
All systems normal