Please note that API data availability depends on your subscription level. Not all data is available on the free plan. Make sure you have the correct subscription level. They start at £19.99/month.
This article builds upon the previous one, “Build a Financial Trading Dashboard with Python Django”, where I introduced the Python Django framework, and we created our first project and application. That tutorial guided you through the process of creating a treemap of market indices, with the ability to drill down into the market indices to view their constituents.
In this article, we will enhance the application by addressing some minor improvements, such as decoupling the hardcoded API keys from the code. Additionally, we will implement functionality to allow viewing of historical market data, and a market search.
As this new functionality is closely related to what was done in the previous article, we will use the same “spglobal” application instead of creating a new application within the Django project.
Quick jump:
Decoupling the API keys
There are several ways to achieve this, but I’m going to demonstrate “the Django way”.
The first step is to create a .env
file in the root of our project, which should look like this…
EODHD_API_TOKEN=your_api_key
We will use a library called python-decouple, which loads the .env
file, allowing us to access its contents within our application.
python3 -m pip install python-decouple -U
Edit the file, “eodhd_apis/eodhd_apis/settings.py” and make the following adjustments.
from pathlib import Path
from decouple import config
# Load the API token from the .env file
EODHD_API_TOKEN = config('EODHD_API_TOKEN')
Edit the “eodhd_apis/spglobal/views.py” file and make it look as below.
from django.conf import settings
import requests
from django.shortcuts import render, get_object_or_404, redirect
from .models import SPGlobalIndex, IndexConstituent
def fetch_data(request):
api_token = settings.EODHD_API_TOKEN
url = f"https://eodhd.com/api/mp/unicornbay/spglobal/list?api_token={api_token}"
response = requests.get(url)
data = response.json()
for item in data:
SPGlobalIndex.objects.update_or_create(
index_id=item.get("ID"),
defaults={
"code": item.get("Code"),
"name": item.get("Name"),
"constituents": item.get("Constituents"),
"value": item.get("Value"),
"market_cap": item.get("MarketCap"),
"divisor": item.get("Divisor"),
"daily_return": item.get("DailyReturn"),
"dividend": item.get("Dividend"),
"adjusted_market_cap": item.get("AdjustedMarketCap"),
"adjusted_divisor": item.get("AdjustedDivisor"),
"adjusted_constituents": item.get("AdjustedConstituents"),
"currency_code": item.get("CurrencyCode"),
"currency_name": item.get("CurrencyName"),
"currency_symbol": item.get("CurrencySymbol"),
"last_update": item.get("LastUpdate"),
},
)
indices = SPGlobalIndex.objects.all()
return render(request, "spglobal/index.html", {"indices": indices})
def fetch_index_constituents(request, index_code):
api_token = settings.EODHD_API_TOKEN
url = f'https://eodhd.com/api/mp/unicornbay/spglobal/comp/{index_code}?fmt=json&api_token={api_token}'
response = requests.get(url)
data = response.json()
# Extract constituents and general information
constituents = data['Components'].values()
general_info = data['General']
return render(request, 'spglobal/constituents.html', {
'constituents': constituents,
'general_info': general_info
})
When you run your application again, you’ll notice it runs smoothly as before. The key difference is that your private API key is now securely separated in the .env
file.
Market Search API
EODHD APIs has a search API which I want to implement in this app. You can find the official documentation here.
I will need to create a data model for this. This will be different to the previous models because I don’t want to store the data in the database. I just want a structure for the API response to be stored.
We will need to create a model for our new historical data feature. Update the eodhd_apis/spglobal/models.py and include this in the file.
# ADD THIS AT THE TOP
from dataclasses import dataclass
# ADD THIS AT THE END
@dataclass
class SearchData:
Code: str
Exchange: str
Name: str
Type: str
Country: str
Currency: str
ISIN: str
previousClose: float
previousCloseDate: str
As per the same process as before we’ll need to update the urls.py, views.py, and create a new template called search_results.html.
eodhd_apis/spglobal/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("", views.fetch_data, name="fetch_data"),
path(
"constituents/<str:index_code>/",
views.fetch_index_constituents,
name="fetch_index_constituents",
),
path("search/", views.search_market_data, name="search_market_data"),
]
eodhd_apis/spglobal/views.py
import time
from django.conf import settings
from datetime import datetime, timedelta
import requests
from django.shortcuts import render, get_object_or_404, redirect
from .models import SPGlobalIndex, IndexConstituent, SearchData
def fetch_data(request):
api_token = settings.EODHD_API_TOKEN
url = f"https://eodhd.com/api/mp/unicornbay/spglobal/list?api_token={api_token}"
response = requests.get(url)
data = response.json()
for item in data:
SPGlobalIndex.objects.update_or_create(
index_id=item.get("ID"),
defaults={
"code": item.get("Code"),
"name": item.get("Name"),
"constituents": item.get("Constituents"),
"value": item.get("Value"),
"market_cap": item.get("MarketCap"),
"divisor": item.get("Divisor"),
"daily_return": item.get("DailyReturn"),
"dividend": item.get("Dividend"),
"adjusted_market_cap": item.get("AdjustedMarketCap"),
"adjusted_divisor": item.get("AdjustedDivisor"),
"adjusted_constituents": item.get("AdjustedConstituents"),
"currency_code": item.get("CurrencyCode"),
"currency_name": item.get("CurrencyName"),
"currency_symbol": item.get("CurrencySymbol"),
"last_update": item.get("LastUpdate"),
},
)
indices = SPGlobalIndex.objects.all()
return render(request, "spglobal/index.html", {"indices": indices})
def fetch_index_constituents(request, index_code):
api_token = settings.EODHD_API_TOKEN
url = f"https://eodhd.com/api/mp/unicornbay/spglobal/comp/{index_code}?fmt=json&api_token={api_token}"
response = requests.get(url)
data = response.json()
constituents = data["Components"].values()
general_info = data["General"]
return render(
request,
"spglobal/constituents.html",
{"constituents": constituents, "general_info": general_info},
)
def search_market_data(request):
query = request.GET.get("query", "")
api_token = settings.EODHD_API_TOKEN
url = f"https://eodhd.com/api/search/{query}?api_token={api_token}&fmt=json"
response = requests.get(url)
data = response.json() if response.status_code == 200 else []
results = [SearchData(**item) for item in data]
return render(
request, "spglobal/search_results.html", {"results": results, "query": query}
)
eodhd_apis/spglobal/templates/spglobal/search_results.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Search Results</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.datatables.net/1.10.21/css/dataTables.bootstrap4.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/1.7.1/css/buttons.bootstrap4.min.css">
<style>
body {
background-color: #343a40;
color: #ffffff;
}
.table {
background-color: #212529;
}
.table a {
text-decoration: underline !important;
}
.table th,
.table td {
color: #ffffff;
}
.btn-dark {
background-color: #6c757d;
border-color: #6c757d;
}
a {
color: #ffffff !important;
text-decoration: none;
background-color: transparent;
}
a:hover {
color: #adb5bd !important;
}
.page-item.active .page-link {
color: #ffffff !important;
background-color: #495057 !important;
border-color: #495057 !important;
}
.page-link {
color: #ffffff !important;
background-color: #6c757d !important;
border-color: #343a40 !important;
}
.page-link:hover {
color: #adb5bd !important;
background-color: #5a6268 !important;
border-color: #343a40 !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
color: #ffffff !important;
background-color: #6c757d !important;
border: 1px solid #343a40 !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
background-color: #5a6268 !important;
border: 1px solid #343a40 !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
color: #ffffff !important;
background-color: #495057 !important;
border: 1px solid #343a40 !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover {
background-color: #6c757d !important;
color: #ffffff !important;
}
</style>
</head>
<body>
<div class="container mt-5">
<h1 class="mb-4 text-light">Search Results</h1>
<div class="table-responsive">
<table id="resultsTable" class="table table-dark table-striped table-bordered">
<thead class="thead-dark">
<tr>
<th>Code</th>
<th>Exchange</th>
<th>Name</th>
<th>Type</th>
<th>Country</th>
<th>Currency</th>
<th>ISIN</th>
<th>Previous Close</th>
<th>Previous Close Date</th>
</tr>
</thead>
<tbody>
{% for item in results %}
<tr>
<td>
<a href="/historical/{{ item.Code }}.{{ item.Exchange }}/d/" class="text-white">
{{ item.Code }}
</a>
</td>
<td>{{ item.Exchange }}</td>
<td>{{ item.Name }}</td>
<td>{{ item.Type }}</td>
<td>{{ item.Country }}</td>
<td>{{ item.Currency }}</td>
<td>{{ item.ISIN }}</td>
<td>{{ item.previousClose }}</td>
<td>{{ item.previousCloseDate }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<a href="{% url 'fetch_data' %}" class="btn btn-dark mt-4">Back to Index</a>
</div>
https://code.jquery.com/jquery-3.5.1.min.js
</body>
</html>
This will have the same look and feel of the previous constituents table from the previous article.
I will update the constituents page to allow you to reach this new page. What is quite exciting about this is it works with all markets on EODHD. If you know the EODHD APIs code for any market with historical data you can view it.
Updating the Constituents results
What would be nice now is to be able to access either the market constituents OR the historical data from the treemap. I’ve made the updates to make this possible in the
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{ general_info.Name }} Constituents</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.datatables.net/1.10.21/css/dataTables.bootstrap4.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/1.7.1/css/buttons.bootstrap4.min.css">
<style>
body {
background-color: #343a40;
color: #ffffff;
}
.table {
background-color: #212529;
}
.table th, .table td {
color: #ffffff;
}
.btn-dark {
background-color: #6c757d;
border-color: #6c757d;
}
a {
color: #ffffff !important;
text-decoration: none;
background-color: transparent;
}
a:hover {
color: #adb5bd !important;
}
.page-item.active .page-link {
z-index: 3;
color: #ffffff !important;
background-color: #495057 !important;
border-color: #495057 !important;
}
.page-link {
color: #ffffff !important;
background-color: #6c757d !important;
border-color: #343a40 !important;
}
.page-link:hover {
color: #adb5bd !important;
background-color: #5a6268 !important;
border-color: #343a40 !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
color: #ffffff !important;
background-color: #6c757d !important;
border: 1px solid #343a40 !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
background-color: #5a6268 !important;
border: 1px solid #343a40 !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
color: #ffffff !important;
background-color: #495057 !important;
border: 1px solid #343a40 !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled,
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover {
background-color: #6c757d !important;
color: #ffffff !important;
}
.code-link {
color: #ffffff;
text-decoration: underline !important;
}
.code-link:hover {
color: #adb5bd;
text-decoration: none !important;
}
</style>
</head>
<body>
<div class="container mt-5">
<h1 class="mb-4 text-light">{{ general_info.Name }} ({{ general_info.Code }}) Constituents</h1>
<table id="constituentsTable" class="table table-dark table-striped table-bordered">
<thead class="thead-dark">
<tr>
<th>Code</th>
<th>Name</th>
<th>Sector</th>
<th>Industry</th>
<th>Weight</th>
</tr>
</thead>
<tbody>
{% for constituent in constituents %}
<tr>
<td>
<a href="/historical/{{ constituent.Code }}.US/d/" class="code-link">
{{ constituent.Code }}
</a>
</td>
<td>{{ constituent.Name }}</td>
<td>{{ constituent.Sector }}</td>
<td>{{ constituent.Industry }}</td>
<td>{{ constituent.Weight }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<a href="{% url 'fetch_data' %}" class="btn btn-dark mt-4">Back to Index List</a>
</div>
https://code.jquery.com/jquery-3.5.1.min.js
https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.1/dist/umd/popper.min.js
https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js
https://cdn.datatables.net/1.10.21/js/jquery.dataTables.min.js
https://cdn.datatables.net/1.10.21/js/dataTables.bootstrap4.min.js
https://cdn.datatables.net/buttons/1.7.1/js/dataTables.buttons.min.js
https://cdn.datatables.net/buttons/1.7.1/js/buttons.bootstrap4.min.js
https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.3/jszip.min.js
https://cdn.datatables.net/buttons/1.7.1/js/buttons.html5.min.js
https://cdn.datatables.net/buttons/1.7.1/js/buttons.print.min.js
<script>
$(document).ready(function() {
$('#constituentsTable').DataTable({
paging: true,
searching: true,
ordering: true,
info: true,
lengthMenu: [10, 25, 50, 100],
order: [[4, "desc"]],
dom: 'Bfrtip',
buttons: [
{
extend: 'excel',
text: 'Export to Excel'
}
]
});
});
</script>
</body>
</html>
“Gotcha”
You will notice that the constituents page returns a Code like AAPL. The historical data requires you to include the exchange like .US. This information is not available in the constituents list. The way I got this working was to append .US to the code. This should work in almost call cases. The problem may happen if for example an index had any cryptocurrency included as I know that uses .CC as a prefix. I’ve done a lot of testing and I’ve yet to find one that doesn’t work with a .US prefix but I just wanted you to be aware of the limitation. If EODHD APIs decide to add this additional information to the constituents results, then this would be an easy fix.
Next Steps
The next article will explore treemap alternatives that can be used instead of D3.js.