Framework per il backtest di strategie con DataInvestor, Jupyter Notebook e Docker

Framework per il backtest di strategie con DataInvestor, Jupyter Notebook e Docker

Sommario

In questo articolo descriviamo come configurare un framework per il backtest di strategie di trading algoritmico, cioè un ambiente di ricerca di strategie di investimento che si basa sul framework open-source DataInvestor all’interno di un Jupyter Notebook. Vediamo come isolare questo ambiente di ricerca e le sue dipendenze utilizzando Docker, con Docker Compose. Nel prossimo articolo descriviamo come implementare un esempio di strategia con DataInvestor, la strategia di asset allocation tattica Momentum Top N. Per seguire questo tutorial è necessario installare Docker e Docker Compose. Il sito web  di Docker offre alcuni eccellenti tutorial di installazione specifici per ogni sistema operativo.

L’interfaccia Jupyter Notebook consente agli utenti di creare e condividere documenti che contengono codice, equazioni, grafici e testo. Questo è molto utile nello sviluppo e nell’ottimizzazione delle strategie perchè offre  visualizzazioni integrate dei dati. E’ possibile ripetere rapidamente i backtest usando variabili diverse e confrontare facilmente le modifiche esaminando i report di performance delle strategie.

L’utilizzo dei contenitori, come Docker, permette agli sviluppatori di comprimere un’applicazione con tutte le sue dipendenze in un’unica unità software standardizzata. In questo modo le applicazioni  possono funzionare in qualsiasi ambiente, sia su un laptop locale, su un server di test o nel cloud. Docker supporta anche CI/CD (Continuous Integration/Continuous Deployment), consentendo agli sviluppatori di creare, testare e distribuire rapidamente applicazioni senza preoccupparsi del sistema ospite sottostante.

Se si vuole approfondire Docker, il loro sito Web offre alcune risorse eccellenti, tra cui:

Impostazione della struttura delle directory

Non esiste un modo giusto o sbagliato per impostare una struttura di directory per le applicazioni dockerizzate, anche se in genere è meglio attenersi alle migliori pratiche di sviluppo software. Di seguito suggeriamo un metodo modulare, scalabile e mantenibile.

L’applicazione che vogliamo creare, che abbiamo chiamato datainvestor-notebooks, contiene una sottodirectory per l’orchestrazione e una per i componenti. L’organizzazione delle directory per componenti consente agli sviluppatori di isolare le funzionalità in parti distinte e gestibili singolarmente. Ogni componente può essere sviluppata, testata e distribuita in modo indipendente, in modo da semplificare gli aggiornamenti e il debug. Questa modularità si basa sui principi delle architetture a microservizi, dove ogni servizio è incapsulato nel proprio contenitore. Questo approccio permette agli sviluppatori di aggiungere facilmente più componenti con i Dockerfile contenuti all’interno di ciascun servizio aggiuntivo.

La directory di orchestrazione conterrà il file docker-compose.yml per gestire i servizi, le reti e i volumi. Mantenere gli script di orchestrazione (come i file Docker Compose o le configurazioni YAML di Kubernetes) in directory separate aiuta negli aspetti di distribuzione e gestione delle applicazioni. Questa separazione garantisce che le configurazioni operative non siano mescolate con la logica dell’applicazione, semplificando la distribuzione in ambienti diversi.

Framework per il backtest di strategie con DataInvestor, Jupyter Notebook e Docker

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 DataTrading.info.

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 creare il framework per il backtest di strategie di trading 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. Abbiamo scelto di utilizzare l’immagine completa di Ubuntu piuttosto che una versione leggera come Ubuntu Base, poiché include le librerie necessarie per eseguire determinati programmi. Dato che vogliamo visualizzare le curve di equity delle strategie di trading algoritmico, questa scelta permette di accedere a tutte le librerie richieste. Per approfondire come scegliere un’immagine di base appropriata per il proprio Dockerfile è possibile consultare questo sito.

I comandi di un Dockerfile

Dopo aver selezionato l’immagine di base possiamo impostare le variabili di ambiente. Il prefisso ENV indica a docker di impostare una variabile di ambiente che sarà disponibile in tutte le  successive fasi del processo di compilazione.

  • PYTHONWRITEBYTECODE 1: impedisce a Python di scrivere file .pyc. All’interno di un contenitore le invocazioni dei programmi Python e delle loro dipendenze avvengono una sola volta, quindi di solito non è necessario memorizzare il bytecode. Se, tuttavia, vengono generati più processi Python, archiviare i file .pyc potrebbe essere più efficiente.
  • PYTHONUNBUFFERED 1: forza l’unbuffering degli stream stderr e stdout, a vantaggio del debug nei log di Docker. Assicura che l’output di Python venga inviato direttamente al terminale senza prima memorizzarlo nel buffer; questo garantisce che nessun output venga conservato da qualche parte e mai scritto nel caso in cui l’applicazione Python si arresti in modo anomalo.
  • TZ=Europa/Rome: dato che usiamo i dati di serie temporali per il backtest, è necessario impostare il fuso orario corretto. E’ necessario impostarlo in base alla propria posizione.

Con RUN eseguiamo un comando per aggiornare l’impostazione del fuso orario dell’immagine di base. Quindi aggiorniamo tutti i pacchetti e le librerie presenti nell’immagine base e installiamo build-essential, vim e Python3 con pip3. Infine il comando WORKDIR imposta /app come directory di lavoro per il progetto. In questa directory saranno eseguite tutte le successive istruzioni RUN, CMD, ENTRYPOINT, COPY e ADD. Dopo aver attivato il contenitore possiamo navigare su /app e vedere la cartella dei requisiti che verrà aggiunta al Dockerfile nella sezione successiva.

Definire il Docker Compose

Ora dobbiamo definire il docker-compose.yml per concludere la creazione del contenitore Docker. Aggiungiamo quanto segue al 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 DataTrading. 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 creare il framework per il backtest di strategie di trading 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/datatrading-info/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 il backtest di strategie di trading algoritmico

Abbiamo installato DataInvestor e Jupyter Notebooks in un contenitore Docker. 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.

Framework per il backtest di strategie con DataInvestor, Jupyter Notebook e Docker

Attivare i notebook all’interno del Dockerfile

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.

In questo articolo abbiamo descritto come creare un  ambiente di ricerca di backtesting, cioè un framework per il backtest di strategie di trading algoritmico. 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.

Benvenuto su DataTrading!

Sono Gianluca, ingegnere software e data scientist. Sono appassionato di coding, finanza e trading. Leggi la mia storia.

Ho creato DataTrading per aiutare le altre persone ad utilizzare nuovi approcci e nuovi strumenti, ed applicarli correttamente al mondo del trading.

DataTrading vuole essere un punto di ritrovo per scambiare esperienze, opinioni ed idee.

SCRIVIMI SU TELEGRAM

Per informazioni, suggerimenti, collaborazioni...

Scroll to Top