Fetching stock data using the AlphaVantage API

Accessing the Alpha Vantage API

For gathering real stock data, we’ll be using the free Alpha Vantage API. For fetching data over this API, you need to register for a free API key, as we did in chapter 13. 

Note: The free API plan allows you to make up to 5 API requests per minute and 500 requests per day. 

Visit www.alphavantage.co and click on “Get your free API key today”. Then, fill out the contact form and click on “Get Free API Key”.

A text shows up, showing you your personal API key. Let’s save this key by creating a new Swift file in our “Helper” group and calling it “DownloadHelper.swift”.

Inside this file, declare a constant called “apiKey” and assign the key you just generated to it as a String.

let apiKey = "*your API key*"

Similar to the Flickr API, the Alpha Vantage API allows us to request information about specific stocks by sending a request in the form of a URL. We then receive the requested data in JSON format.

Visit the Alpha Vantage API documentation to learn what types of requests are possible. For now, we want to gather financial data about a specific stock on a daily basis. According to the section “Daily” in the documentation, we can use several URL parameters for specifying the request.

A typical URL would look like this:

In our DownloadHelper.swift file, let’s create a function that generates the URL for us. Since the only dynamic value should be the stock symbol, the function is as follows:

func generateRequestURL(stockSymbol: String) -> String {
    return "https://www.alphavantage.co/query?function=TIME_SERIES_INTRADAY&symbol=\(stockSymbol)&interval=15min&outputsize=full&apikey=\(apiKey)"
}

Paste the returned String to your preferred browser and replace the stock symbol with, for instance, “AAPL” for Apple, Inc. and your personal API Key. If you open this URL, you’ll see what the JSON structure looks like.

Let’s incorporate the resulting JSON structure into an appropriate data model we’ll use when fetching the JSON later on. We already discussed this topic in chapters 11 and 13, so make sure you check them out if you’re not sure what’s going on. Again, you can use the quicktype.io tool to accomplish this as we did in the last chapter.

Now, let’s create a new Swift file named “JSONModel.swift”, place it into the “Model” group and insert the following struct (note that TimeSeries is a substruct of TimeSeriesJSON).

struct TimeSeriesJSON: Decodable {
    let timeSeries: [String: TimeSeries]
    
    private enum CodingKeys : String, CodingKey {
        case timeSeries = "Time Series (15min)"
    }
    
    struct TimeSeries: Decodable {
        let open, close, high, low: String
        
        private enum CodingKeys : String, CodingKey {
            case open = "1. open"
            case high = "2. high"
            case low = "3. low"
            case close = "4. close"
        }
    }
}

Setting up the Download Manager

We need to create a download manager to fetch and parse the JSON data and to notify observing views once we are finished. For this purpose, create a new Swift file named “DownloadManager.swift”, place it into the “Model” group as well and import the SwiftUI framework. In this file, create a class also named “DownloadManager” that conforms to the ObservableObject protocol.

import SwiftUI

class DownloadManager: ObservableObject {
    
}

In our DownloadManager class, we need to declare a property that holds an array containing all the fetched and parsed financial information as DataEntry instances.

class DownloadManager: ObservableObject {
    
    var dataEntries = [DataEntry]()
    
}

As said, once we finished fetching the data, we need to notify all observing views. For this purpose, let’s add a corresponding @Published property to our DownloadManager.

@Published var dataFetched = false 

In our ContentView, replace the stockData property with an instance of the DownloadManager as an @ObservedObject. Now we can use the dataEntries in the downloadManager for passing it down to the Header and Chart:

struct ContentView: View {
    
    @ObservedObject var downloadManager: DownloadManager
    
    var body: some View {
        VStack {
            Header(stockData: downloadManager.dataEntries)
            Chart(dataSet: downloadManager.dataEntries)
                .frame(height: 300)
            //...
        }
            .navigationTitle("StockX")
    }
}

Make sure you adjust the ContentView_Previews struct as well.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(downloadManager: DownloadManager())
    }
}

In our StockListRow struct, we also need a DownloadManager to pass it down to the ContentView and display the pricing information.

struct StockListRow: View {
    
    @ObservedObject var downloadManager: DownloadManager
    
    var body: some View {
        HStack {
            NavigationLink(destination: ContentView(downloadManager: downloadManager)) {
                VStack(alignment: .leading) {
                    //...
                }
                Spacer()
                VStack(alignment: .trailing) {
                    Text(String(format: "%.2f", getPercentageChange(stockData: downloadManager.dataEntries)) + "%")
                        //...
                    Text("$" + String(format: "%.2f", downloadManager.dataEntries.last?.close ?? 0))
                        //...
                }
            }
        }
    }
}

In our StockList, we create the actual DownloadManager instance we pass all the way down to the ContentView when initializing the StockListRow.

List {
    StockListRow(downloadManager: DownloadManager())
}

Okay, let’s run our app to see if that works. Oops! After we tap on the StockListRow to navigate to the ContentView our app crashes because of an “Index out of range” error in our Chart view! This is because that GraphGraphGradient, and PriceLegend try to utilize elements in a dataSet that is currently empty. Therefore, we want to initialize these shapes only under the condition that the dataSet isn’t empty:

struct Chart: View {
    
    let dataSet: [DataEntry]
    
    var body: some View {
        ZStack(alignment: .trailing) {
            if !dataSet.isEmpty {
                //...
            }
        }
    }
}

If you re-run the app, you see that everything works fine now, and only the Grid gets displayed when navigating to the ContentView.

Great! Now we’re finally ready to fetch real stock data by using the Alpha Vantage API.

Fetching real-world stock data β¬‡οΈ 

In our DownloadManager class, let’s create a function to fetch the stock data using the Alpha Vantage API.

private func fetchData(stockSymbol: String) {
              
}

Before starting the download, we need to make sure that our dataFetched property is false.

private func fetchData(stockSymbol: String) {
        
    dataFetched = false
        
}

Below the fetchData function, we declare another function to actually download and parse the JSON data.

private func downloadJSON(stockSymbol: String) -> [DataEntry] {

        
}

As we did in the last chapter, we mark this function with the async throws keywords to tell SwiftUI that we want this function to be performed asynchronous and that it can throw errors.

private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {

        
}

Accordingly, we need to define an enum conforming to the Error protocol and insert it into our DownloadManager. Two errors are conceivable here: First, that we cannot generate a valid URL or second, that we cannot generate usable DataEntry instances.

enum FetchError: Error {
    case badURL
    case badEntries
}

Let’s call the downloadJSON function and assign its return to the dataEntries, weeklyEntries and dailyEntries properties when calling the fetchData function by writing:

private func fetchData(stockSymbol: String) {
        
    dataFetched = false
        
    dataEntries = try await downloadJSON(stockSymbol: stockSymbol)
        
}

As we have learned in the last chapter, asynchronous functions cannot be directly embedded in a synchronous context like our fetchData function. So to make this possible we have to use a Task again.

Task {
    dataEntries = try await downloadJSON(stockSymbol: stockSymbol)
}

We start the JSON download process by initializing an empty DataEntry array.

private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {

    dataEntries = [DataEntry]()
        
}

Next, we try to create an URL by using the generateRequestURL function we created earlier. If this fails, we throw the .badURL FetchError.

private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {

    dataEntries = [DataEntry]()
        
    guard let url = URL(string: generateRequestURL(stockSymbol: stockSymbol)) else { throw FetchError.badURL}

}

Then, we create a request out of our url and start an asynchronous URLSession.

private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {

    dataEntries = [DataEntry]()
        
    guard let url = URL(string: generateRequestURL(stockSymbol: stockSymbol)) else { throw FetchError.badURL}
    let request = URLRequest(url: url)
    let (data, _) = try await URLSession.shared.data(for: request)

}

After we received the data, we parse the JSON using a JSONDecoder instance and our data model.

private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {

    dataEntries = [DataEntry]()
        
    guard let url = URL(string: generateRequestURL(stockSymbol: stockSymbol)) else { throw FetchError.badURL}
    let request = URLRequest(url: url)
    let (data, _) = try await URLSession.shared.data(for: request)

    let parsedJSON = try JSONDecoder().decode(TimeSeriesJSON.self, from: data)

}

After we successfully parsed the JSON, we want to cycle through every timeSeries instance in it to create a DataEntry out of it and append it to the dataEntries array.

But before we can do this, we need to make sure that we’re able to create Date objects out of the Strings contained in the parsedJSON. To do this, we need a Date extension that lets us format such a String into a Date.

For this purpose, create a new Swift file called “Extensions.swift” and place it into the “Helper” group. Inside this file, add the following Date extension.

extension Date {
    init(_ dateString:String, dateFormat: String) {
        let dateStringFormatter = DateFormatter()
        dateStringFormatter.dateFormat = dateFormat
        dateStringFormatter.locale = Locale.init(identifier: "en_US_POSIX")
        let date = dateStringFormatter.date(from: dateString)!
        self.init(timeInterval:0, since:date)
    }
}

Back in our downloadJSON function, we can now create a DataEntry out of every timeSeries element inside the parsedJSON.

private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {

    //...

    for timeSeries in parsedJSON.timeSeries {
        dataEntries.append(DataEntry(date: Date(timeSeries.key, dateFormat: "yyyy-MM-dd HH:mm:ss"), close: (timeSeries.value.close as NSString).doubleValue))
    }

}

We can check if we cycled through every instance by comparing the number of our DataEntries to the number of timeSeries in the parsedJSON. If these are equal, we know that we’re finished with the downloading and parsing process.

private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {

    //...

    for timeSeries in parsedJSON.timeSeries {
        dataEntries.append(DataEntry(date: Date(timeSeries.key, dateFormat: "yyyy-MM-dd HH:mm:ss"), close: (timeSeries.value.close as NSString).doubleValue))
    }

    if dataEntries.count == parsedJSON.timeSeries.count {
        
    }

}

We need to make sure that the elements in our DataEntries array follow the correct order depending on their Date properties.

private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {

    //...

    for timeSeries in parsedJSON.timeSeries {
        dataEntries.append(DataEntry(date: Date(timeSeries.key, dateFormat: "yyyy-MM-dd HH:mm:ss"), close: (timeSeries.value.close as NSString).doubleValue))
    }

    if dataEntries.count == parsedJSON.timeSeries.count {
        dataEntries.sort(by: {$0.date.compare($1.date) == .orderedAscending})
    }

}

Filtering the data entries

Right now, our dataEntries array contains every time series we extracted from the parsed JSON. However, we only want to display the stock information for the last seven days. Therefore, we need to filter the dataEntries before turning the dataFetched property to true.

For this purpose, we need to add some more lines of codes to our downloadJSON function before returning the dataEntries.

But how do we know which time series in our DataEntries array are from within the last seven days? It’s simple, we just need to refer to the last element in our dataEntries array and compare each remaining element to it.

We need to make sure that there is actually a last element we can refer to. To do this, we use a guard statement in our filterDataEntries function. After that, we create a new array that will only contain the filtered entries.

private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {

    //...

    guard let lastDateOfData = dataEntries.last?.date else { throw FetchError.badEntries }

}

To compare each entry to the lastDateOfData we need to create another Date extension. So, let’s open the Extensions.swift file and add another function to it.

extension Date {
    //...
    
    func isInLastNDays(lastDate: Date, dayRange n: Int) -> Bool {
        let startDate = Calendar.current.date(byAdding: .day, value: -n, to: Date())!
        return (min(startDate, lastDate) ...  max(startDate, lastDate)).contains(self)
    }
}

Using this function, we can check if a specific Date is within the last n-days of another given Date. 

Back in our downloadJSON function: We create a new filteredEntries array, cycle through every element in the dataEntries array, compare its date to the lastDateOfDate and only add those entries to the filteredEntries that are in the specified time range of 7 days. Then we assign the content of the filteredEntries to the dataEntries.

private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {

    //...

    guard let lastDateOfData = dataEntries.last?.date else { throw FetchError.badEntries }

    var filteredEntries = [DataEntry]()

    for entry in dataEntries {
        if entry.date.isInLastNDays(lastDate: lastDateOfData, dayRange: 7) {
            filteredEntries.append(entry)
        }
    }

    dataEntries = filteredEntries

}

Finally, we can return the – now filtered – dataEntries.

private func downloadJSON(stockSymbol: String) async throws -> [DataEntry] {

    //...

    return dataEntries
}

Awesome, we finished our downloadJSON function!

Now back to the fetchData function that calls the downloadJSON function: after we waited for the downloadJSON to finish, we notify all observing views by turning the dataFetched property to true. We do this asynchronously as well. For this purpose, we use the DispatchQueue.main.async wrapper.

Task {
    dataEntries = try await downloadJSON(stockSymbol: stockSymbol)
    DispatchQueue.main.async {
        withAnimation {
            self.dataFetched = true
        }
    }
}

We want to start fetching the stock data once the DownloadManager gets initialized. To do this, add an init function to the DownloadManager class that calls the fetchData function by using a provided stock symbol.

init(stockSymbol: String) {
    fetchData(stockSymbol: stockSymbol)
}

Awesome! We’re finished preparing our DownloadManager for downloading, parsing, and filtering the JSON that contains the relevant financial information.

Next, we need to adjust our interface to reflect the relevant data once it’s available.

Displaying fetched data in our UI πŸ‘

Now, we need to adjust our views to display the fetched data. To do this, provide the StockListRow instance in your StockList with the stockSymbol that the downloadManager should use to call the fetchData function.

List {
    StockListRow(downloadManager: DownloadManager(stockSymbol: "AAPL"))
}

Next, we need to provide the ContentView_Previews struct with such a stockSymbol as well.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView(downloadManager: DownloadManager(stockSymbol: "AAPL"))
    }
}

Finally, we want our ContentView to display the Chart only once the data got fetched. To do this, we utilize the dataFetched property like this:

VStack {
    if downloadManager.dataFetched {
        Header(stockData: downloadManager.dataEntries)
        Chart(dataSet: downloadManager.dataEntries)
            .frame(height: 300)
    }
    //...
}

Accordingly, we only want to show the pricing information in our StockListRow once the data got fetched too. 

HStack {
    NavigationLink(destination: ContentView(downloadManager: downloadManager)) {
        //...
        if downloadManager.dataFetched {
            VStack(alignment: .trailing) {
                Text(String(format: "%.2f", getPercentageChange(stockData: downloadManager.dataEntries)) + "%")
                    .font(.custom("Avenir", size: 14))
                    .fontWeight(.medium)
                    .foregroundColor(.green)
                Text("$" + String(format: "%.2f", downloadManager.dataEntries.last?.close ?? 0))
                    .font(.custom("Avenir", size: 26))
            }
        }
    }
}

Okay, the moment has come: Let’s run our app to see if everything works fine.

Awesome! Just a few moments after we launch the app, our DownloadManager fetches the pricing information about the specified stock by accessing the Alpha Vantage API. Once the process is finished, our StockList and ContentView get notified, and they eventually show us the relevant pricing information and the corresponding chart graph!

Recap

Wow, that was a lot of work. But we are almost finished with our StockX app.

So far we’ve learned how to use the Alpha Vantage API to download relevant financial information and parse the corresponding JSON for our purposes.

We now use actual stock information to display the corresponding charts on a weekly basis.

Finally, we will add a way to display the chart on a daily basis as well.

 

One reply on “Fetching stock data using the AlphaVantage API”

Just a heads up. I’m using the latest version of Xcode (Version 12.4 (12D4e)) and in the NavigationLink of body in StockList.swift I needed to provide the label: argument when creating the NavigationLink(). From my debugging to get it working it appears that the reference to getPercentageChange() and the downloadManager @ObservedObject was what was upseting Xcode if I excluded the label: argument.

What worked perfectly for me was the following with the important change being encapsulating the the leading and trailing VStack()s and Spacer() inside the label: argument:

struct StockListRow: View {

@ObservedObject var downloadManager: DownloadManager

var body: some View {
HStack {

NavigationLink(
destination: ContentView(downloadManager: downloadManager),
label: {
if downloadManager.dataFetched {
VStack(alignment: .leading) {
Text(“AAPL”)
.font(.custom(“Avenir”, size: 20))
.fontWeight(.medium)
Text(“Apple Inc”)
.font(.custom(“Avenir”, size: 16))
}
Spacer()
VStack(alignment: .trailing) {
Text(String(format: “%.2f”, getPercentageChange(stockData: downloadManager.dataEntries)) + “%”)
.font(.custom(“Avenir”, size: 14))
.fontWeight(.medium)
.foregroundColor(.green)
Text(“$” + String(format: “%.2f”, downloadManager.dataEntries.last?.close ?? 0))
.font(.custom(“Avenir”, size: 26))
}
}
}
)
}
}
}

The app works perfectly and I was able to successfully fetch the AAPL numbers from Alpha Vantage and display them correctly. Totally cool! πŸ™‚

Leave a Reply

Your email address will not be published. Required fields are marked *