Create a Financial Trading Dashboard Using Python and Django

This is the first article in a four-part series on creating and enhancing the application.

EODHD APIs
10 min readDec 9, 2024
Build a Financial Trading Dashboard with Python Django

Python Django is a high-level framework designed to streamline the development of web applications. While we often use Python and typically opt for Flask due to its flexibility and minimal constraints, Django provides a more structured and opinionated approach, promoting standardized solutions.

This tutorial is intended to introduce you to Django and also serve as a helpful refresher for our team.

Recently, EODHD APIs launched a new product called “Indices Historical Constituents Data”. This API is relatively new and has not yet been integrated into the official Financial Python library. It offers two primary functions: listing indices and retrieving the constituents of each index. For example, the S&P 500 index, represented by the code “GSPC.INDX,” consists of 503 stocks instead of 500. This difference arises because some companies have multiple stock classes included in the index.

The API provides up to 12 years of historical and current data for global indices such as the S&P 500, S&P 600, S&P 100, S&P 400, and other key industry indices. It includes detailed information on current constituents and historical changes, making it an invaluable resource for analyzing market trends and formulating long-term investment strategies. Designed for seamless integration, the API delivers structured data in JSON format, ideal for developers and analysts working on financial projects.

For more details, visit the official product pages on EODHD’s marketplace or forum, where the API’s features and details are discussed further.

Prerequisites

  • Install Python 3, preferably version 3.9 or higher. This tutorial uses version 3.9.6 on macOS.
  • Install an IDE such as Visual Studio Code, which is free and highly recommended.
  • Create a virtual environment: python3 -m venv venv.
  • Upgrade Python PIP: python3 -m pip install --upgrade pip.
  • Initialize the virtual environment: source venv/bin/activate.
  • Install Django: python3 -m pip install django -U.

Django — The Basics

Django projects consist of one or more apps. An app can be thought of as a component within a project. For this tutorial, we will create a project named “eodhd_apis” that includes a single app, “spglobal”.

D3.js

We’ll use the popular JavaScript visualization library D3.js to create a treemap.

Bootstrap

For the constituents’ data table, we’ll use the Bootstrap JavaScript library. Bootstrap simplifies adding features like export options, sorting, and pagination to webpage tables.

Setting Up the Django Project

Django includes a utility within the virtual environment, django-admin, which we’ll use to create our project.

(venv) $ django-admin startproject eodhd_apis  
(venv) $ cd eodhd_apis

Django will generate a project script, “manage.py,” which is used to create and manage applications.

(venv) eodhd-django-webapp $ python manage.py startapp spglobal

Add the App to the Settings

To enable the app, edit the “eodhd_apis/eodhd_apis/settings.py” file and include “spglobal” in the INSTALLED_APPS list.

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

To map the API endpoint to a Django model, we will create a model named “SPGlobalIndex”. This model corresponds to the data retrieved from the following API endpoint:

https://eodhd.com/api/mp/unicornbay/spglobal/list?api_token=<YOUR_API_KEY>

This model will serve as the foundation for storing and working with the API data within the Django application.

To map the following API endpoint, we will create a Django model named “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 include the two models provided below. The API fields have already been mapped to the appropriate 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

After creating the model, execute the following commands to create and apply the database migrations.

(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 key actions occur during this process:

  1. Database Creation:
    A file named “db.sqlite3” will appear in the project root. SQLite is the default database for Python Django, and this file stores your database data. If any issues arise, you can delete this file and allow the app to rebuild it using data from the API.
  2. Migration Files:
    In the “eodhd_apis/spglobal/migrations” directory, files are generated to track changes in the database schema. The first file is typically named “0001_initial.py”. If you delete the database file mentioned above, ensure you also remove all migration files to avoid conflicts.

Below are some helpful model and database diagnostic commands for troubleshooting if needed:

(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 update it as shown below. Ensure that 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
})

If the file does not exist, create “eodhd_apis/spglobal/urls.py” to manage the routes.

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",
),
]

Update the file “eodhd_apis/eodhd_apis/urls.py” with the following changes as highlighted.

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

Set up the template directory structure: “eodhd_apis/spglobal/templates/spglobal”.

Then, create the file: “eodhd_apis/spglobal/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>

Create the file: “eodhd_apis/spglobal/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 to enable adding indices through Django’s admin interface.

from django.contrib import admin
from .models import SPGlobalIndex

admin.site.register(SPGlobalIndex)

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 a clickable treemap of indices using D3.js.
  • Detail Page (/constituents/<id>/):
    Displays additional details about the selected index.

If everything is functioning correctly, the output should look as described.

Admin Interface (Optional)

Data can be added manually via the admin interface or dynamically through an API call.

To access the admin interface at http://127.0.0.1:8000/admin, a superuser account must be created.

(venv) eodhd_apis $ python manage.py createsuperuser 

If you manually add the indices in the admin interface and reload http://127.0.0.1:8000, they will appear. However, the app is designed to automatically populate data using API calls to EODHD APIs.

In “eodhd_apis/spglobal/admin.py”, only “SP Global Indices” is registered in the admin interface. This is because the index list remains static during app runtime, while the constituents vary based on user interaction. Initially, data was added directly to the model, but this approach slowed down the app unnecessarily. Since the API is not configured to store data in the model and the model remains empty, displaying it in the admin interface is not practical.

Conclusion

This Python Django application can be expanded to incorporate additional endpoints from EODHD APIs. Adding fundamental data could provide more value, and implementing features to browse historical data with selectable timeframes could also enhance functionality.

This article is the first in a series of four about building the application. Stay tuned for the next articles to explore further developments and enhancements.

Please note that this article is for informational purposes only and should not be taken as financial advice. We do not bear responsibility for any trading decisions made based on the content of this article. Readers are advised to conduct their own research or consult with a qualified financial professional before making any investment decisions.

For those eager to delve deeper into such insightful articles and broaden their understanding of different strategies in financial markets, we invite you to follow our account and subscribe for email notifications.

Stay tuned for more valuable articles that aim to enhance your data science skills and market analysis capabilities.

--

--

EODHD APIs
EODHD APIs

Written by EODHD APIs

eodhd.com — stock market fundamental and historical prices API for stocks, ETFs, mutual funds and bonds all over the world.

No responses yet