Building a simple candlestick chart using Docker, FastAPI, and Vue 3 - Part 7

Photo by Rob Fuller on Unsplash

Building a simple candlestick chart using Docker, FastAPI, and Vue 3 - Part 7

Fetching data from our server

The next step in our journey is to hook up our frontend to the backend. In the last article we made things look a bit better and we set up some logic so that a loading spinner appears when we click on the submit button. Now we need to use the handleSubmit method to actually fetch data from our server.

Spin up the server

In case we don't recall from Part 1 of this series, our server is set up in a Python environment using FastAPI. If our directory structure is like this

.
├── fast-api-vue-stock
│   ├── client
│   └── server
│       └── main.py

we can open a terminal/command prompt and type the following

cd /path/to/server
env\Scripts\activate
uvicorn main:app --reload

This assumes that we already have the same setup as was described in Part 1 of the tutorial. By typing this then our server should be running at port 8000.

Connecting to the server

Create a querystring

For those who may not recall, the format of the url that we will be consuming in the backend is of the format /quote/{ticker}/{interval}. Then, any date information, as it is optional, is passed in via query parameters in the format YYYY-mm-dd. So, within the handleSubmit method in Selections.vue we first need to create a query object and add the start and end dates if they exist.

const query = {}
if (!!state.startDate) query.start = state.startDate;
if (!!state.endDate) query.end = state.endDate;

Then, we can just map through the keys of the query and use the encodeURIComponent built-in method to convert our dates into the correct format. It should be noted that since we're only doing this with two values the method that will be presented below may be a little overkill, but the idea is that this method can be used for any number of query parameters so it's worth it to present.

const queryString = Object.keys(query)
  .filter((key) => !!query[key])
  .map((key) => {
    return (
      `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`
    );
  })
  .join('&')

To see what this method does, let's use the following object as an example

query = {
  "hello": "world",
  "hashnode": [1,2,3],
  "vue": {
    "is": "cool"
  },
  "fastapi": "is as well"
}

If we then look at the resulting string, we get 'hello=world&hashnode=1%2C2%2C3&vue=%5Bobject%20Object%5D&fastapi=is%20as%20well' and we notice that there is an object%20Object where the value for the vue key should be. This shows us that we can't just use this method for any deeply nested structures, but it instead works for simple key-value stores. If we instead set

query = {
  "hello": "world",
  "hashnode": [1,2,3],
  "vue": ["is", "cool"],
  "fastapi": "is as well"
}

then the resulting string would look like 'hello=world&hashnode=1%2C2%2C3&vue=is%2Ccool&fastapi=is%20as%20well' which would work much better. There is an argument that you shouldn't need to do something like this when you're passing in the data yourself, but I've found that it can't hurt to just be extra careful with these sorts of things. Now we should have a valid query string for our start and end dates, if they exist.

Convert human-readable interval into yfinance style

The next step is to convert the interval into the style that yfinance needs. For human readability, we're presenting our intervals as "Daily", "Weekly", and "Monthly", but these won't work in yfinance and we should actually get errors if we try to pass these values in because we've set the allowable input values to be one of 1d, 1wk, or 1mo. So, this is as simple as just using a nested JavaScript ternary operator.

const interval = 
  state.interval === 'Daily'
    ? '1d'
    : state.interval === 'Weekly'
    ? '1wk'
    : '1mo';

This sets the interval constant to 1d if the state's interval value is Daily, otherwise if the state's interval value is Weekly it sets it to 1wk, otherwise it sets it to 1mo. The equivalent code in if statements would be

let interval;

if (state.interval === 'Daily') {
  interval = '1d'
} else if (state.interval === 'Weekly') {
  interval = '1wk'
} else {
  interval = '1mo'
}

Create the URL string and add the query if it exists

We can now create the URL that will be used to fetch the data from our server. We first set up the basic url with the symbol and interval values

let url = `http://localhost:8000/quote/${state.symbol}/${interval}`;

and we then need to append any query string, if it exists.

if (queryString.length) url = `${url}?${queryString}`;

We do this because we need to put a ? at the beginning of the query string. The next step is to fetch the data from the server using the built-in fetch API.

const response = await fetch(url);
const res = await response.json();

Parse the dates and format data

Parse dates

The next step is to update the state's startDate and endDate with the values obtained from the server. This is done because one or both of the dates input by the user may have been weekends or holidays, or any other myriad possibilities. We assume that the data we've received from the server contains valid dates, so the simplest way to do this is to just set state.startDate to the earliest date, and state.endDate to the latest date. First, we want to create an anonymous function that will format the dates into the format we're currently using on the site.

const formatDate = (d) => new Date(d).toISOString().substr(0, 10);

and then we sort the dates, and set each one

const dates = res.data.map((e) => e.date).sort();
state.startDate = formatDate(dates[0])
state.endDate = formatDate(dates[dates.length - 1])

Format the data into our desired output

This last step is going to come slightly out of left field; we are going to format the data in a format that will eventually be used by our charting platform. We're going to have the x coordinates be the dates and the y coordinates be the data, whether volume or stock prices.

const data = res.data.map((e) => ({
  x: new Date(e.date),
  y: e.data
}));
const volData = res.data.map((e) => ({
  x: new Date(e.date),
  y: e.volume ?? 0
}));

The entire code of the handleSubmit method should now look like this

const handleSubmit = async () => {
  v$.value.$validate();
  if (!v$.value.$invalid) {
    loading.value = true;

    // Create the querystring
    const query = {}
    if (!!state.startDate) query.start = state.startDate;
    if (!!state.endDate) query.end = state.endDate;

    const queryString = Object.keys(query)
      .filter((key) => !!query[key])
      .map((key) => {
        return (
          `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`
        );
      })
      .join('&')

    // Convert human-readable interval into yfinance style
    const interval =
      state.interval === 'Daily'
        ? '1d'
        : state.interval === 'Weekly'
          ? '1wk'
          : '1mo';

    // Create URL string and add query if it exists
    let url = `http://localhost:8000/quote/${state.symbol}/${interval}`;
    if (queryString.length) url = `${url}?${queryString}`;
    const response = await fetch(url);
    const res = await response.json();

    // Parse dates
    const formatDate = (d) => new Date(d).toISOString().substr(0, 10);
    const dates = res.data.map((e) => e.date).sort();
    state.startDate = formatDate(dates[0])
    state.endDate = formatDate(dates[dates.length - 1])

    // Format the data
    const data = res.data.map((e) => ({
      x: new Date(e.date),
      y: e.data
    }));
    const volData = res.data.map((e) => ({
      x: new Date(e.date),
      y: e.volume ?? 0
    }));

    loading.value = false;
  }
};

You'll notice that we removed the setTimeout and added an entry at the bottom to set the loading spinner to false. Also, we needed to make the function itself async in order to utilize the await keyword.

In the next article we'll discuss error handling, because having this type of functionality not wrapped in try and catch blocks is a recipe for disaster. We'll also add in a dialog to alert the user when an error has taken place.