Serving MOEX(Moscow exchange) API with FastAPI

Recently I discovered the MOEX API, a few weeks ago to be more precise, with the recent events I thought that it may be interesting for you to know about this API. I say interesting, because you can consume the data that this API provides, basically data from the Moscow Exchange, and see how this Exchange behaves with the current crisis in Ukraine and the sanctions imposed from other countries to Russia. I’m not going to make an analysis of this data here, that’s not my interest, you can make it by your own ).

A point that I want to clarify, this is a technical post, don’t spec anything else from the reading of this post. If I wish to write about politics, I would write about Cuba, where I lived 26 years of my life, and where there’s a lot of things to talk about. I barely know Belarus, that’s where I’m leaving now. Saying that, let’s start describing the requirements.

Requirements

  • Docker
  • An account in the MOEX API, this is optional, the API allow you to consumes the data that I will be using for free. The creation of the account is totally free, no credit card needed.

Having docker, is really not necessary to have anything else on your machine maybe. Just in case it would be easier for you to have:

Introduction

The MOEX API have a lot of interesting data, at least for me, maybe for you is boring. For our use case, what I’m going to do is basically expose the data that you can use to make some candlestick charts. For this purpose I wrote a pretty simple Pyton client to interact with this API, you can find it here, is called moexiss.

Let’s focus on what is important for this tutorial. 1. I hope, you will see how to define a “model” of the data you want to return on a given endpoint of the API. 2. Define parameters to receive on GET endpoints. 3. Build a Docker container with your FastAPI API, very redundant I know.

Endpoints

boards

Our API will expose the boards available in the stock engine and the index market. For this particular endpoint you won’t need to provide any parameter. We are going to return the list of boards according to this structure.

[
  {
    "id": 0,
    "board_group_id": 0,
    "boardid": "string",
    "title": "string",
    "is_traded": true
  }
]

securities

In case of the securities endpoint you must provide the boardid parameter in the GET request, and the API will return you a list of available securities, with the following data.

[
  {
    "secid": "string",
    "boardid": "string",
    "name": "string",
    "decimals": 0,
    "shortname": "string",
    "annualhigh": 0,
    "annuallow": 0,
    "currencyid": "string",
    "calcmode": "string"
  }
]

candles

Last but not least, we are going to serve you also data that you could use to draw some candlestick charts. For this case you will need to provide the following parameters:

  1. board, board id that can be fetched with the boards endpoint.
  2. security, security id that can be fetched with the securities endpoint.
  3. start_date and end_date, in this case you must provide a range of date that you want to fetch data from. You must take into consideration that there are days where the index market is not working, so if you choose one of these days don’t be surprised if you don’t get any data.

The structure of the data to be returned would be as follow:

{
  "board": "string",
  "security": "string",
  "start_date": "string",
  "end_date": "string",
  "records": [
    {
      "open": 0,
      "close": 0,
      "high": 0,
      "low": 0,
      "value": 0,
      "volume": 0,
      "begin": "string",
      "end": "string"
    }
  ]
}

Code

The tree of the final directory would be this:

.
├── app.py
├── Dockerfile
├── models
│   ├── __init__.py
│   └── model.py
├── moexiss
│   ├── client.py
│   ├── __init__.py
│   └── README.md
└── requirements.txt

The code related to moexiss could be found here, feel free to git clone.

First, let’s define our models to be returned on the endpoints of the API. In order to define these models we are going to need the following imports,

importing pydantic and typing

from pydantic import BaseModel
from typing import List, Optional

Board model File: models/model.py


class Board(BaseModel):
    id: int
    board_group_id: int
    boardid: str
    title: str
    is_traded: bool

Now given this model, the API needs to return a List of this model, for that reason our endpoint that will return the boards need to be defined in the following way:

boards endpoint definition

File: app.py


@app.get("/boards", response_model=List[Board])
async def boards():
    # code related to processing boards
    resp = client.boards("stock", "index")
    data = form_data(resp)
    boards = [Board(**d) for d in data]

    return boards

The most important detail here, is the response_model provided in the decorator call, in this case we are using the List from typing library, and specifying that this endpoint will return a list of the model previously defined. Pretty straight forward.

Here I make use of an utility function called form_data, the definition of this function would be:

def form_data(resp):
    return [dict(zip(resp["columns"], record)) for record in resp["data"]]

Is a small transformation of the data fetched from the MOEX API.

Security model

File: models/model.py


class Security(BaseModel):
    secid: str
    boardid: str
    name: str
    decimals: int
    shortname: str
    annualhigh: float
    annuallow: float
    currencyid: str
    calcmode: str = None

The endpoint for this case, would receive one parameter called boardid that would be a string, and would return a List of Security, the code related to it would be as follows:

securities endpoint definition

File: app.py


@app.get("/securities", response_model=List[Security])
async def securities(boardid: str):
    resp = client.securities("stock", "index", boardid)
    resp["columns"] = [col.lower() for col in resp["columns"]]
    data = form_data(resp)
    secs = [Security(**d) for d in data]

    return secs

Notice that the parameter is defined in the definition of the endpoint handler. In case you failed to provide this parameter, FastAPI by default will return you and 422 HTTP Unprocessable Entity. In the body of this response you could find the reason why the API is returning you this error, is just awesome, that requires zero effort by your part.

In the case of the last endpoint, candles, is basically the same, only that here I will need to define another model called Record

Security model

File: models/model.py


class Record(BaseModel):
    open: float
    close: float
    high: float
    low: float
    value: float
    volume: float
    begin: str
    end: str

class Candle(BaseModel):
    board: str
    security: str
    start_date: str
    end_date: str

    records: Optional[List[Record]]

Our endpoint would be similar to the previous one, only with more parameters

candles endpoint definition

File: app.py


@app.get("/candles", response_model=Candle)
async def candles(board: str, security: str, start_date: str, end_date: str):
    args = {
        "engine": "stock",
        "market": "index",
        "board": board,
        "security": security,
        "start_date": start_date,
        "end_date": end_date,
    }
    resp = client.candle_data(**args)

    candle = Candle(**args)
    candle.records = form_data(resp)

    return candle

Docker container

Now for our docker image I use the Pyton3.9 image as the base image, honestly I tried to make other way at first. I tried to use a distroless container, but is a pain in the ass, really annoying. This is only with Pyton, with Golang images works like a charm, no complaining about that, but with Pyton man that’s another story. The main problem that you will find is with uvicorn, no more comment on this matters, if you know how let me know.

Back to business, the Dockerfile is pretty simple

FROM python:3.9

WORKDIR /app

COPY . /app

RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "80"]

The image built with this dockerfile is about 991MB, I don’t like images so big, that’s the reason on trying to use distroless container.

Note

I don’t provide the whole code on purpose, is to force you to write the missing parts by yourself. There’s no too much to add really, but at least you do something.