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


blog photo
Marius SprengerSeptember 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. 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.

Title Card for "Building a Mean Reversion Strategy with the lemon.markets API"

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.

GIF of Stephen Colbert

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
3import json
4from dotenv import load_dotenv
5class RequestHandler:
6    load_dotenv()
7    url_data: str = os.environ.get("BASE_URL_DATA")
8    url_trading: str = os.environ.get("BASE_URL_TRADING")
9    def get_data_trading(self, endpoint: str):
10        response = requests.get(self.url_trading + endpoint,
11                                headers={
12                                    "Authorization": "Bearer " + os.environ.get("API_KEY")
13                                })
14        return response.json()
15    def get_data_data(self, endpoint: str):
16        """
17        :param endpoint: {str} only append the endpoint to the base url
18        :return:
19        """
20        response = requests.get(self.url_data + endpoint,
21                                headers={
22                                    "Authorization": "Bearer " + os.environ.get("API_KEY")
23                                })
24        return response.json()
25    def put_data(self, endpoint: str):
26        response = requests.put(self.url_trading + endpoint,
27                                headers={
28                                    "Authorization": "Bearer " + os.environ.get("API_KEY")
29                                })
30        return response.json()
31    def post_data(self, endpoint: str, data):
32        response = requests.post(self.url_trading + endpoint,
33                                 json.dumps(data),
34                                 headers={
35                                     "Authorization": "Bearer " + os.environ.get("API_KEY")
36                                 })
37        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| API_KEY |  Your API Key | 
4|MIC| Market Identifier Code of Trading Venue|
5|BASE_URL_TRADING | Base URL of our paper money API |
6|BASE_URL_DATA | Base URL of our paper money API |

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 our documentation. 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 = "", expires_at: str = "", quantity: int = 0, side: str = "",
6                 stop_price: int = 0, limit_price: int = 0, uuid: str = "", venue: str = ""):
7        self.isin = isin
8        self.expires_at = expires_at
9        self.quantity = quantity
10        self.side = side
11        self.stop_price = stop_price
12        self.limit_price = limit_price
13        self.venue = venue
14        self.uuid = uuid
15   
16    def place_order(self):
17        order_details = {
18            "isin": self.isin,
19            "expires_at": self.expires_at,
20            "side": self.side,
21            "quantity": self.quantity,
22            "venue": self.venue,
23        }
24        load_dotenv()
25        endpoint = f'orders/'
26        response = self.post_data(endpoint, order_details)
27        return response
28    def get_orders(self):
29        load_dotenv()
30        endpoint = f'orders/'
31        response = self.get_data_trading(endpoint)
32        return response
33    def activate_order(self, order_uuid):
34        load_dotenv()
35        endpoint = f'orders/{order_uuid}/activate/'
36        response = self.post_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
4from datetime import datetime, timedelta
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 = (datetime.now() -timedelta(days=7)).strftime('%Y-%m-%d')
13        to_date = datetime.today().strftime('%Y-%m-%d')
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),                                       
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

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

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 support@lemon.markets or join our Slack community. We’d love to hear from you 🍋 💛 .

Marius

You might also be interested in

blog photo

Profiting in Bear Markets with 5 Useful Algorithmic Trading Strategies

You may have heard the term “bear market” being thrown around a lot recently on the news and online. Perhaps you have looked at your favourite stock picks and only see red. In this article, we explain what bear markets are and algorithmic strategies that can be advantageous during a bear market.

blog photo

The market maker explained

Market Makers are crucial to provide liquidity to stock exchanges. In this blog post, we talk about what Market Makers do and why they are useful.

blog photo

5 (+1) YouTube channels for FinTech enthusiasts 

YouTube is a great way to learn about new things, including financial education or coding. Therefore, in this article we’d like to introduce you to 5 YouTube channels to level up your trading literacy.

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.

Need any help?
Ask a question in our CommunityAsk a question in our CommunityGet started with our DocumentationGet started with our DocumentationGet inspired on our BlogGet inspired on our Blog
© lemon.markets 2022Privacy PolicyImprint
Systems are down

As a tied agent under § 3 Sec. 2 WplG on the account and under the liability of DonauCapital Wertpapier GmbH, Passauer Str. 5, 94161 Ruderting (short: DonauCapital), lemon.markets GmbH offers you the receipt and transmission of orders for clients (§ 2 Sec. 2 Nr. 3 WpIG) of financial instruments according to § 3 Sec. 5 WpIG as well as brokerage of accounts.