Python Django is a high-level framework designed to facilitate the rapid development of web applications. I frequently work with Python, and when I opt for a framework, I typically choose Flask due to its flexibility, which allows me to build solutions with fewer restrictions. In contrast, Django is more opinionated, promoting a highly structured approach where there is often “one right way” to implement a solution.
I thought this tutorial would be useful for introducing you to Django, while also serving as a helpful refresher for myself.
Recently, EODHD APIs introduced a new product called “Indices Historical Constituents Data”. This API is quite new and has not yet been integrated into the official Python library. It provides two key functions: one for listing indices and another for retrieving the constituents of each index. To illustrate, consider the S&P 500 index, which is denoted by the code “GSPC.INDX.” Although it’s called the S&P 500, the index actually comprises of 503 stocks. This discrepancy exists because while there are 500 companies in the index, some companies have more than one type of stock class included.
This new API offers up to 12 years of historical and current data for various global indices, including the S&P 500, S&P 600, S&P 100, and S&P 400, as well as key industry indices. The API provides detailed information such as the list of current constituents and any historical changes over time. Such data, which is difficult to find elsewhere, is highly valuable for in-depth analysis of market trends and long-term investment strategies. It’s designed for seamless integration and delivers structured data in JSON format, making it ideal for developers and analysts working on financial projects.
For more information, you can visit the official product pages on EODHD’s marketplace or forum, where they discuss the features and details of the API in more depth here and here.
Please note, API data availability depends on your subscription plan. Some data isn’t included in the free plan. Visit our pricing page to find the package that fits your needs.
Quick jump:
Prerequisites
- Install Python 3, ideally 3.9 or better. I’m using 3.9.6 on my Mac.
- Install an IDE. I’m using Visual Studio Code, it’s free and excellent.
- Create a virtual environment called “python3 -m venv venv”.
- Upgrade Python PIP, “python3 -m pip install –upgrade pip”.
- Initialise the virtual environment “source venv/bin/activate”.
- Install Django using Python PIP. “python3 -m pip install django -U”.
Django – The Basics
Django has the concept of a project and app(s). A project may be made up of one or more app(s). You could consider an app as a component within the project. For my tutorial series I’m going to create a project called “eodhd_apis”. The project will consist of one more components or apps. The first app we will create will be called “spglobal”.
D3.js
I’m going to use a popular javascript visualisation library called D3.js to create my treemap.
Bootstrap
I’m going to use a popular javascript data tables library called Bootstrap to create my constituents data table. Bootstrap is great as it’s easy to add export, sorting and pagination functionality to webpage tables.
Setting up our Django project
Django has a utility (within the virtual environment) called “django-admin” that we’ll use to create our project.
(venv) $ django-admin startproject eodhd_apis
(venv) $ cd eodhd_apis
Django will create a project script called “manage.py” that we’ll use to create and start our application.
(venv) eodhd-django-webapp $ python manage.py startapp spglobal
Add the app to the Settings
Edit the file, “eodhd_apis/eodhd_apis/settings.py” and “spglobal” to the INSTALLED_APPS.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'spglobal',
]
Creating our data models
We will want to map this API endpoint to a model (“SPGlobalIndex”):
https://eodhd.com/api/mp/unicornbay/spglobal/list?api_token=<YOUR_API_KEY>
We will want to map this API endpoint to a model (“IndexConstituent”):
https://eodhd.com/api/mp/unicornbay/spglobal/comp/GSPC.INDX?fmt=json&api_token=<YOUR_API_KEY>
Edit the file “eodhd_apis/spglobal/models.py” and add the two models below. I have done the hard part of mapping the API fields to the correct data types in the models.
from django.db import models
class SPGlobalIndex(models.Model):
index_id = models.CharField(max_length=50, unique=True)
code = models.CharField(max_length=50)
name = models.CharField(max_length=255)
constituents = models.IntegerField()
value = models.FloatField()
market_cap = models.FloatField(null=True, blank=True)
divisor = models.FloatField(null=True, blank=True)
daily_return = models.FloatField()
dividend = models.FloatField(null=True, blank=True)
adjusted_market_cap = models.FloatField(null=True, blank=True)
adjusted_divisor = models.FloatField(null=True, blank=True)
adjusted_constituents = models.IntegerField()
currency_code = models.CharField(max_length=10)
currency_name = models.CharField(max_length=50)
currency_symbol = models.CharField(max_length=10)
last_update = models.DateField()
def __str__(self):
return self.name
class IndexConstituent(models.Model):
index = models.ForeignKey(
SPGlobalIndex, on_delete=models.CASCADE, related_name="components"
)
code = models.CharField(max_length=10)
name = models.CharField(max_length=255)
sector = models.CharField(max_length=50)
industry = models.CharField(max_length=100)
weight = models.FloatField()
def __str__(self):
return self.name
Once the model is created, run the following commands to create and migrate the database.
(venv) eodhd_apis % python3 manage.py makemigrations
Migrations for 'spglobal':
spglobal/migrations/0001_initial.py
- Create model SPGlobalIndex
- Create model IndexConstituent
(venv) eodhd_apis % python3 manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, spglobal
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
Applying admin.0001_initial... OK
Applying admin.0002_logentry_remove_auto_add... OK
Applying admin.0003_logentry_add_action_flag_choices... OK
Applying contenttypes.0002_remove_content_type_name... OK
Applying auth.0002_alter_permission_name_max_length... OK
Applying auth.0003_alter_user_email_max_length... OK
Applying auth.0004_alter_user_username_opts... OK
Applying auth.0005_alter_user_last_login_null... OK
Applying auth.0006_require_contenttypes_0002... OK
Applying auth.0007_alter_validators_add_error_messages... OK
Applying auth.0008_alter_user_username_max_length... OK
Applying auth.0009_alter_user_last_name_max_length... OK
Applying auth.0010_alter_group_name_max_length... OK
Applying auth.0011_update_proxy_permissions... OK
Applying auth.0012_alter_user_first_name_max_length... OK
Applying sessions.0001_initial... OK
Applying spglobal.0001_initial... OK
Two actions are going to be done here:
- You will see a file called “db.sqlite3” created in your project root. SQLite is the default database for Python Django. This is where your database data will be stored. If you ever run into trouble you can delete this file and let the app rebuild it from API data.
- In your “eodhd_apis/spglobal/migrations” directory, you will see files created to keep track of the change log in the database. The first file would typically be called “0001_initial.py”. If you ever get stuck and have to delete the database mentioned above, make sure you delete all the transaction files as well.
Here are some useful model and database diagnostic commands if you need them…
(venv) eodhd_apis % python3 manage.py showmigrations
admin
[X] 0001_initial
[X] 0002_logentry_remove_auto_add
[X] 0003_logentry_add_action_flag_choices
auth
[X] 0001_initial
[X] 0002_alter_permission_name_max_length
[X] 0003_alter_user_email_max_length
[X] 0004_alter_user_username_opts
[X] 0005_alter_user_last_login_null
[X] 0006_require_contenttypes_0002
[X] 0007_alter_validators_add_error_messages
[X] 0008_alter_user_username_max_length
[X] 0009_alter_user_last_name_max_length
[X] 0010_alter_group_name_max_length
[X] 0011_update_proxy_permissions
[X] 0012_alter_user_first_name_max_length
contenttypes
[X] 0001_initial
[X] 0002_remove_content_type_name
sessions
[X] 0001_initial
spglobal
[X] 0001_initial
(venv) eodhd_apis % python3 manage.py shell
Python 3.9.6 (default, Oct 18 2022, 12:41:40)
[Clang 14.0.0 (clang-1400.0.29.202)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from spglobal.models import SPGlobalIndex
>>> SPGlobalIndex.objects.all()
<QuerySet []>
>>>
(venv) eodhd_apis % python manage.py dbshell
SQLite version 3.37.0 2021-12-09 01:34:53
Enter ".help" for usage hints.
sqlite> .schema spglobal_indexconstituent
CREATE TABLE IF NOT EXISTS "spglobal_indexconstituent" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "code" varchar(10) NOT NULL, "name" varchar(255) NOT NULL, "sector" varchar(50) NOT NULL, "industry" varchar(100) NOT NULL, "weight" real NOT NULL, "index_id" bigint NOT NULL REFERENCES "spglobal_spglobalindex" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "spglobal_indexconstituent_index_id_5d694d08" ON "spglobal_indexconstituent" ("index_id");
sqlite>
Creating our Views
Edit the “eodhd_apis/spglobal/views.py” file and make it look as below. Make sure you replace <YOUR_API_KEY> with your subscription API key.
import requests
from django.shortcuts import render, get_object_or_404, redirect
from .models import SPGlobalIndex, IndexConstituent
def fetch_data(request):
url = "https://eodhd.com/api/mp/unicornbay/spglobal/list?api_token=<YOUR_API_KEY>"
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):
url = f'https://eodhd.com/api/mp/unicornbay/spglobal/comp/{index_code}?fmt=json&api_token=<YOUR_API_KEY>'
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
})
Create this file if it doesn’t exist to handle the routes, “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",
),
]
And update this file with the following changes in bold, “eodhd_apis/eodhd_apis/urls.py”.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("spglobal.urls")),
]
Treemap with D3.js
Create the template directory structure, “eodhd_apis/spglobal/templates/spglobal”
And create the file, “eodhd_apis/spgobal/templates/spglobal/index.html”.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Market Indices</title>
https://d3js.org/d3.v6.min.js
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #343a40;
color: #ffffff;
}
h1 {
color: #ffffff;
text-align: center;
margin-top: 20px;
}
#treemap {
margin: 0 auto;
}
.node {
border: solid 1px white;
font: 10px sans-serif;
line-height: 12px;
overflow: hidden;
position: absolute;
text-align: center;
}
a {
text-decoration: underline;
color: #ffffff;
}
a:hover {
color: #d3d3d3;
}
</style>
</head>
<body>
<div class="container mt-5">
<h1>Market Indices</h1>
<div id="treemap"></div>
</div>
<script>
const data = {
"name": "Indices",
"children": [
{% for index in indices %}
{
"index_id": "{{ index.index_id }}",
"code": "{{ index.code }}",
"name": "{{ index.name }}",
"constituents": {{ index.constituents }}
},
{% endfor %}
]
};
const width = 1140;
const height = window.innerHeight * 0.8;
const treemap = d3.treemap()
.size([width, height])
.padding(1)
.round(true);
const root = d3.hierarchy(data)
.sum(d => d.constituents)
.sort((a, b) => b.constituents - a.constituents);
treemap(root);
const svg = d3.select("#treemap")
.append("svg")
.attr("width", width)
.attr("height", height)
.style("font", "10px sans-serif");
const cell = svg.selectAll("g")
.data(root.leaves())
.enter().append("g")
.attr("transform", d => `translate(${d.x0},${d.y0})`);
const colorScale = d3.scaleOrdinal(d3.schemeCategory10);
function getFontSize(tileWidth, tileHeight) {
const minSize = Math.min(tileWidth, tileHeight);
return Math.max(10, Math.min(16, minSize * 0.15));
}
function wrapText(text, width) {
text.each(function() {
const text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
lineHeight = 1.1;
let word,
line = [],
lineNumber = 0,
y = text.attr("y"),
dy = parseFloat(text.attr("dy")) || 0,
tspan = text.text(null).append("tspan").attr("x", 3).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", 3).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
cell.append("rect")
.attr("id", d => d.data.id)
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0)
.attr("fill", d => colorScale(d.data.index_id))
.attr("stroke", "#ffffff")
.on("click", d => {
window.location.href = `/index/${d.data.code}/`;
});
cell.append("foreignObject")
.attr("x", 3)
.attr("y", 3)
.attr("width", d => d.x1 - d.x0)
.attr("height", d => d.y1 - d.y0)
.append("xhtml:div")
.style("font-size", d => getFontSize(d.x1 - d.x0, d.y1 - d.y0) + "px")
.style("color", "#ffffff")
.style("overflow", "hidden")
.html(d => `<a href="/constituents/${d.data.index_id}/">${d.data.code}</a> (${d.data.constituents})`);
</script>
</body>
</html>
And create the file, “eodhd_apis/spgobal/templates/spglobal/constituents.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>{{ 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;
}
</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>{{ constituent.Code }}</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>
Add Data to the Database
In “eodhd_apis/spglobal/admin.py”, register the SPGlobalIndex and IndexConstituent models so you can add index via Django’s admin interface:
from django.contrib import admin
from .models import SPGlobalIndex
admin.site.register(SPGlobalIndex)
Now, run the server:
(venv) eodhd_apis % python3 manage.py runserver
Watching for file changes with StatReloader
Performing system checks...
System check identified no issues (0 silenced).
October 09, 2024 - 09:13:47
Django version 4.2.16, using settings 'eodhd_apis.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
Summary:
- Main Page (
/
): Displays the clickable treemap of people using D3.js. - Detail Page (
/constituents/<id>/
): Displays additional information about the selected index.
If everything is working as expected (so far), it should look like this…
Admin Interface (Optional)
We can either add the data manually through the admin interface or we can add it dynamically using an API call.
In order to access the admin interface (http://127.0.0.1:8000/admin), we will need to create a super user account.
(venv) eodhd_apis $ python manage.py createsuperuser
If you manually add the indices here and reload http://127.0.0.1:8000, you will see them. I’ve developed the app to automatically populate the data using API data from EODHD APIs.
You will notice that I only registered “Sp global indices” in the admin interface “eodhd_apis/spglobal/admin.py”. The reason for this is the index list is static as the app runs. The constituents however changes depending on what is being viewed. I initially was adding the data to the model, but it makes the app unnecessarily slow for no good reason. The API data is not coded to store to the model, and as the model is empty, it doesn’t make sense to see it in the admin interface.
Conclusion
This Python Django application can be further enhanced to include many more EODHD APIs endpoints. The fundamentals data could be an interesting addition. It may also be interesting to be able to browse through historical data using a selectable timeframe.