In questo articolo configuriamo un framework per strategie d’investimento quantitative e sistematiche, cioè un ambiente di ricerca di strategie basato sul framework open-source DataInvestor, all’interno di un Jupyter Notebook. Isoliamo questo ambiente di ricerca e le sue dipendenze utilizzando Docker insieme a Docker Compose.
Per seguire questo tutorial, installiamo Docker e Docker Compose. Il sito web di Docker fornisce ottimi tutorial di installazione specifici per ogni sistema operativo.
L’interfaccia Jupyter Notebook ci consente di creare e condividere documenti che contengono codice, equazioni, grafici e testo. Questo risulta molto utile nello sviluppo e nell’ottimizzazione delle strategie perché integra visualizzazioni dei dati. Possiamo ripetere rapidamente i backtest usando variabili diverse e confrontare facilmente le modifiche esaminando i report di performance delle strategie.
Quando usiamo i contenitori come Docker, impacchettiamo un’applicazione con tutte le sue dipendenze in un’unica unità software standardizzata. In questo modo facciamo funzionare le applicazioni in qualsiasi ambiente: sul nostro laptop, su un server di test o nel cloud. Docker supporta anche CI/CD (Continuous Integration/Continuous Deployment), così possiamo creare, testare e distribuire rapidamente le applicazioni senza preoccuparci del sistema ospite sottostante.
Se vogliamo approfondire Docker, troviamo ottime risorse sul loro sito web, tra cui:
- Guide pratiche che ci permettono di acquisire conoscenze specifiche indipendentemente dal punto di partenza.
- Guida completa per chi è all’inizio nell’uso di Docker.
- Serie di video sui concetti di Docker.
- Creazione di un contenitore da zero: un video eccellente che spiega come creiamo e isoliamo i contenitori. Vale la pena guardarlo se siamo curiosi di sapere cosa succede “sotto”!
Impostazione della struttura delle directory
Non esiste un modo giusto o sbagliato per strutturare le directory di un’applicazione dockerizzata, anche se conviene seguire le migliori pratiche di sviluppo software. Qui proponiamo un metodo modulare, scalabile e facile da mantenere.
L’applicazione che vogliamo creare, che chiamiamo datainvestor-notebooks, include una sottodirectory per l’orchestrazione e una per i componenti. Quando organizziamo le directory per componenti, possiamo isolare le funzionalità in parti distinte e facilmente gestibili. Così sviluppiamo, testiamo e distribuiamo ogni componente in modo indipendente, semplificando aggiornamenti e debug. Questa modularità segue i principi delle architetture a microservizi, dove ogni servizio è incapsulato nel proprio contenitore. Questo approccio ci permette di aggiungere facilmente nuovi componenti con Dockerfile inclusi in ciascun servizio aggiuntivo.
Inseriamo nella directory di orchestrazione il file docker-compose.yml, che gestisce servizi, reti e volumi. Quando manteniamo gli script di orchestrazione (come i file Docker Compose o le configurazioni YAML di Kubernetes) in directory separate, facilitiamo la distribuzione e la gestione delle applicazioni. Così evitiamo di mescolare le configurazioni operative con la logica dell’applicazione, rendendo più semplice la distribuzione in ambienti diversi.

Definire i pre-requisiti
Oltre al file Dockerfile e al file docker-compose.yml abbiamo anche una directory per i requisiti contenente base.txt e run_notebooks.sh. Il file base.txt contiene le librerie e i pacchetti python che servono alla nostra applicazione. Il file Run_notebooks.sh è uno script di shell che viene eseguito una volta inizializzato il contenitore docker e attiva lo Jupyter Notebook. Inoltre all’interno della directory components dobbiamo inserire una sottodirectory con il codice sorgente di DataInvestor che può essere scaricato dal repository github di TradingQuant.it.
Il resto di questo articolo prevede di seguire questa struttura di directory e implementa un contesto specifico del percorso all’interno di docker-compose.yml. Se si è sicuri di come modificare i percorsi suggeriamo di seguire la struttura definita qui, altrimenti si può modificare i percorsi per adattarli alla struttura di directory scelta.
Inizializzazione di Docker e Docker Compose
Una volta create le directory e i file richiesti per realizzare un framework per strategie d’investimento quantitative dobbiamo creare un ambiente isolato all’interno di un contenitore Docker. A tale scopo dobbiamo creare un Dockerfile per installare il servizio che contiene DataInvestor. Per inizializzare il contenitore Docker possiamo aggiungere quanto segue al Dockerfile.
# Pull Base Image
FROM ubuntu:22.04
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1
ENV TZ=Europe/Rome
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# Update and Upgrade
RUN apt-get update -y && apt-get upgrade -y
# Add base sysadmin/coding tools
RUN apt-get install -y build-essential vim python3-dev python3-pip
# Set the work directory
WORKDIR /app
Iniziamo inserendo un’immagine base di Ubuntu. Scegliamo di usare l’immagine completa di Ubuntu invece di una versione leggera come Ubuntu Base, perché include tutte le librerie necessarie per eseguire determinati programmi. Poiché vogliamo visualizzare le curve di equity delle strategie di trading algoritmico, questa scelta ci permette di accedere a tutte le librerie richieste. Per approfondire come selezionare un’immagine di base adatta al proprio Dockerfile, possiamo consultare questo sito.
I comandi di un Dockerfile
Dopo aver scelto l’immagine di base, definiamo le variabili di ambiente. Il prefisso ENV dice a Docker di impostare una variabile di ambiente, che resta disponibile in tutte le fasi successive del processo di compilazione.
- PYTHONWRITEBYTECODE 1: impedisce a Python di scrivere file .pyc. Poiché in un contenitore eseguiamo i programmi Python e le loro dipendenze una sola volta, di solito non serve memorizzare il bytecode. Tuttavia, se attiviamo più processi Python, archiviare i file .pyc può risultare più efficiente.
- PYTHONUNBUFFERED 1: forza l’unbuffering degli stream stderr e stdout, utile per il debug nei log di Docker. Garantiamo così che l’output di Python venga inviato direttamente al terminale, senza passare dal buffer; in questo modo, nessun output rischia di rimanere bloccato nel buffer se l’applicazione Python si arresta in modo anomalo.
- TZ=Europa/Rome: dato che lavoriamo con serie temporali per il backtest, impostiamo il fuso orario corretto. Dobbiamo adattarlo alla nostra posizione geografica.
Con RUN eseguiamo il comando per aggiornare l’impostazione del fuso orario dell’immagine di base. Poi aggiorniamo tutti i pacchetti e le librerie già presenti, e installiamo build-essential, vim e Python3 con pip3. Infine, con il comando WORKDIR impostiamo /app come directory di lavoro per il progetto. In questa directory eseguiamo tutte le istruzioni successive come RUN, CMD, ENTRYPOINT, COPY e ADD. Dopo aver avviato il contenitore, possiamo navigare su /app e vedere la cartella dei requisiti, che aggiungeremo al Dockerfile nella prossima sezione.
Definire il Docker Compose
Ora definiamo il file docker-compose.yml per completare la creazione del contenitore Docker, che conterrà l’ambiente di ricerca per le strategie d’investimento quantitative. Inseriamo quanto segue nel file docker-compose.yml all’interno della sottodirectory di orchestrazione.
services:
dt-backtest:
build:
context: ../components
volumes:
- ../dt_stratdev_notebooks:/app/notebooks
- ../dt_stratdev_data:/data
ports:
- 8888:8888
Iniziamo specificando i servizi, che chiamiamo dt come abbreviazione di TradingQuant. Forniamo al Compose di Docker una specifica di build con il context che indica un percorso nelle sottodirectory. Il context è usato per eseguire la build e Docker cercherà un Dockerfile in questa directory. Il percorso può essere assoluto o, come nell’esempio, può essere relativo alla posizione del file docker-compose.yml.
Successivamente dobbiamo definire due volumi per il servizio. I volumi sono archivi persistenti di dati che sono implementati da Docker. Possono essere usati da più servizi. Compose offre molte altre opzioni per la configurazione dei servizi. Nell’esempio abbiamo definito due volumi separati: uno per i notebook e uno per i dati delle serie temporali associati alle strategie. Per semplicità abbiamo creato una directory di notebook e dati a partire dalla root dell’applicazione. Tuttavia, questi percorsi possono essere modificati a piacere.
Nella definizione dei volumi, ogni posizione dell’host deve essere mappato con una posizione nel contenitori. I due percorsi sono separati da due punti (:). La directory dei notebook sarà una sottodirectory di /app all’interno del contenitore e la directory dei dati sarà posizionata nella root del contenitore. Questo approccio progettuale è utile quando usiamo un controllo della versione, come git e desideriamo mantenere i dati al di fuori del repository dell’applicazione.
Infine mappiamo le porte tra l’host e il contenitore. In questo modo possiamo visualizzare i notebook tramite il browser sul computer host collegandosi all’indirizzo “localhost:/8888”. La porta 8888 è la porta non ufficialmente riservata ai notebook Ipython e Jupyter. Per saperne di più sulle porte riservate è possibile consultare wikipedia.
Creare il contenitore Docker
Prima di creare il contenitore per la prima volta possiamo aggiungere la seguente riga al Dockerfile.
CMD tail -f /dev/null
Il comando tail
visualizza nello stream stdout le ultime 10 righe di un file. Con l’opzione -f, tail segue il file in tempo reale, mostrando le nuove righe aggiunte man mano che vengono scritte nel file. Il file /dev/null
è un file speciale in Unix/Linux, detto “buco nero”, dove viene scartato tutto quello che ci viene scritto e quindi è sempre vuoto (EOF). In questo modo tail rimane sempre in esecuzione senza fare nulla. Ha l’effetto di mantenere attivo il contenitore, permettendoci di accedervi e fare il debug, prima di provare a far funzionare Jupyter Notebooks. Una volta completato il Dockerfile rimuoveremo questa riga.
Per realizzare un ambiente di ricerca per strategie d’investimento quantitative dobbiamo creare il contenitore Docker accedendo alla directory di orchestrazione e digitando docker compose up --build -d
nel terminale. Il flag -d permette di eseguire il processo in background e restituire il controllo del terminale. La prima esecuzione impiegherà del tempo perchè si deve creare l’immagine di Ubuntu. Nelle successive esecuzioni del contenitore ometteremo il flag --build
in modo da utilizzare la cache di Docker e velocizzare il processo.
Una volta creato il contenitore, possiamo digitare docker ps
per verificare l’elenco dei contenitori attivi. Il nome del contenitore è nel campo NAMES. Per accedere al contenitore possiamo digitare docker exec --it CONTAINER-NAME /bin/bash
nel terminale, sostituendo CONTAINER-NAME con il nome del contenitore che vogliamo eseguire.
Dopo essere entrati nel contenitore possiamo navigare come faremmo normalmente all’interno di un terminale. Digitando ls
si dovrebbe visualizzare la directory dei notebook, come definito nel docker-compose.yml.
Per uscire dal contenitore è sufficiente digitare exit
. Per arrestare il contenitore si può accedere alla directory di orchestrazione e digitare il comando docker compose down
. Dopo aver terminato il contenitore Docker, siamo pronti per aggiungere alla build i pacchetti di Jupyter Notebooks e DataInvestor.
Installazione delle dipendenze
Ora possiamo specificare le librerie necessarie come requisiti in modo che vengano incluse nel processo di creazione del nostro contenitore. Aggiungiamo quanto segue all’interno del file base.txt
presente nella sottodirectory “Components/Reuirements”.
notebook==7.1
Dobbiamo ora copiare il file nel contenitore ed eseguire pip install. Nel Dockerfile aggiungiamo le seguenti righe prima della riga CMD tail -f /dev/null
.
# Install dependencies
COPY requirements/base.txt /app/
RUN pip3 install -r base.txt
A questo punto dobbiamo includere il codice sorgente del framework open-source DataInvestor che abbiamo creato in questa serie di articoli e reso disponibile su github. Iniziamo posizionandoci all’interno della directory “Components” e cloniamo il repository di github con il seguente comando
git clone https://github.com/tradingquant-it/DataInvestor.git
Aggiorniamo il Dockerfile in modo da copiare i file del codice sorgente di DataInvestor all’interno della workdir del contenitore e il relativo file “requirements.txt” che contiene le dipendenze da installare. Nel Dockerfile aggiungiamo le seguenti righe prima della riga CMD tail -f /dev/null
.
# Install hatchling
RUN pip3 install hatchling
# Install DataInvestor
COPY DataInvestor /app/DataInvestor
RUN pip3 install /app/DataInvestor
Ora possiamo creare nuovamente il contenitore Docker. Accediamo alla directory di orchestrazione e digitiamo docker compose up -d
. Dovremmo notare che i primi cinque passaggi del processo di compilazione vengono memorizzati nella cache. Dopo aver completato la creazione possiamo accedere al sistema digitando docker exec --it CONTAINER-NAME /bin/bash
. Una volta entrati possiamo digitare pip3 freeze | grep datainvestor
e se tutto è stato installato correttamente otteniamo il seguente output datainvestor @ file:///app/DataInvestor
.
Framework per Strategie d’investimento Quantitative
Dopo aver installato DataInvestor e Jupyter Notebooks in un contenitore Docker, possiamo iniziare ad usare l’ambiente di ricerca per strategie d’investimento quantitative. Possiamo attivare Jupyter Notebook utilizzando il seguente comando:
jupyter notebook --ip 0.0.0.0 --no-browser --allow-root --NotebookApp.token=''
Diamo un’occhiata alle opzioni di questo comando:
- –ip 0.0.0.0 –> configura il server notebook per l’ascolto su tutti gli IP.
- –no-browser –> dice a Jupyter Notebook di non aprire una finestra del browser.
- –allow-root –> attiva l’accesso root.
- –NotebookApp.token=” –> sostituisce la password con una stringa vuota.
Dobbiamo tenere presente che permettere l’esecuzione come root all’interno del contenitore Docker e Jupyter Notebook, e disabilitare la password per Jupyter NON sono le migliori pratiche. Tuttavia, è un compromesso accettabile perchè stiamo eseguendo Docker sul nostro computer locale e non in un ambiente più ampio. Se vogliamo trasferire l’applicazione datainvestor-notebook per usarla come parte di un sistema più ampio, dobbiamo rivedere le opzioni di sicurezza.
Dopo aver digitato questo comando nel contenitore Docker è possibile visualizzare i notebook Jupyter nel browser del computer host accedendo a localhost:/8888. Dovremmo vedere il familiare menu dei notebook di Jupyter.

Attivare i notebook all’interno del Dockerfile
Nell’ambiente di ricerca per strategie d’investimeno quantitative possiamo automatizzare l’attivazione di Jupyter Notebook creando uno script bash che può essere eseguito durante la creazione del contenitore. Come in precedenza, dobbiamo uscire dal contenitore e spegnerlo con il comando docker compose down
. Modifichiamo il file run_notebooks.sh presente nella directory components/requirements/ ed inseriamo il comando jupyter notebook descritto in precedenza. Dobbiamo anche aggiungere il comando tail -f /dev/null
per reindirizzare l’output su dev/null e mantenere il contenitore in esecuzione. Aggiungiamo il seguente comando a run_notebooks.sh:
jupyter notebook --ip 0.0.0.0 --no-browser --allow-root --NotebookApp.token='' && tail -f /dev/null
Ora modifichiamo il Dockerfile in modo da copiare lo script bash all’interno del contenitore ed eseguirlo per attivare Jupyter Notebooks. Aggiungiamo quanto segue al Dockerfile e rimuoviamo la riga finale CMD tail -f /dev/null
.
# Run notebooks
COPY requirements/run_notebooks.sh /app/
RUN chmod +x /app/run_notebooks.sh
CMD ./run_notebooks.sh
Ora possiamo ricostruire il contenitore con il comando docker compose up -d
e dovremmo essere in grado di accedere all’ambiente del notebook tramite il browser del computer host collegandosi all’indirizzo localhost/:8888.
Conclusione
In questo articolo abbiamo descritto come creare un ambiente di ricerca di backtesting, cioè un framework per l’analisi di strategie d’investimento quantitative. Nel prossimo articolo descriviamo come implementare un esempio di strategia di investimento utilizzando il framework di backtesting DataInvestor. Vediamo come verificare la strategia Momentum Top N, una strategia di asset allocation tattico, presenti negli esempi di DataInvestor.