Simulazione del Moto Browniano Geometrico con Python

Simulazione del Moto Browniano Geometrico con Python

La generazione di dati sintetici è una tecnica estremamente utile nella finanza quantitativa. Fornisce la possibilità di valutare il comportamento dei modelli utilizzando dati con comportamenti noti. Questa tecnica ha una miriade di applicazioni, come testare il corretto comportamento funzionale dei simulatori di backtesting e consentire  di valutare potenziali scenari alternativi, ad esempio simulare crisi economiche, recessioni e cigni neri.

La generazione di dati sintetici prevede lo sviluppo di un modello del comportamento dei dati. Esistono sostanzialmente due approcci per lo sviluppo di questo modello, che possono anche essere combinati per produrre modelli ancora più sofisticati. Il primo approccio è definito ‘model driven’, dove si prevede di specificare un modello “fisico” del comportamento dell’asset e vengono eseguite simulazioni da questo modello. Le equazioni differenziali stocastiche (SDE) rientrano ampiamente in questo approccio. Il secondo approccio è ‘data driven’, dove si prevede di stimare la distribuzione, ad esempio, dei rendimenti azionari da titoli reali e usare questa distribuzione per creare nuove “serie simulate” dello stesso titolo.

In questo articolo  descriviamo come generare più file CSV di dati sintetici dei prezzi/volumi giornalieri di titoli azionari tramite la soluzione analitica dell’equazione differenziale stocastica del moto browniano geometrico (GBM). Usiamo Python per implementare una classe richiamabile, con cui si interagisce tramite un’interfaccia a riga di comando (CLI) utilizzando la libreria click di Python. Usiamo anche NumPy e Pandas per simulare e “descrivere” i dati.

Lo scopo di questo articolo non è solo quello di dimostrare come simulare i dati dei prezzi/volume, ma anche di delineare come sviluppare un componente di codice a livello produttivo, ad esempio se si scrivesse software in un hedge fund quantitativo. Per brevità non abbiamo incluso una suite completa di unit test di integrazione, ma questo sarebbe un requisito obbligatorio in un contesto istituzionale.

L’articolo procede delineando la struttura generale del programma e successivamente approfondendo i metodi delle singole classi. Il codice operativo completo è riportato alla fine dell’articolo.

Panoramica del codice

La struttura complessiva del codice è conservata in un singolo file, chiamato gbm.py. Iniziamo importando le librerie python necessarie e quindi definiamo la classe che esegue il lavoro, successivamente implementiamo l’interfaccia click di  Python e infine forniamo un punto di ingresso tramite il solito if __name__ == "__main__".

				
					# IMPORTAZIONE LIBRERIE

class GeometricBrownianMotionAssetSimulator:
    def __init__(...):
        ...

    def _SOME_CALCULATION_METHOD(...):
        ...

    def __call__(...):
        # CHIAMATA ALTRI METODI


# DEFINZIONE DEI PARAMETRI PER LA LINEA DI COMANDO CLICK 
def cli(COMMAND_LINE_PARAMS):
    gbmas = GeometricBrownianMotionAssetSimulator(...)
    gbmas()  # Qui si richiama la classe per eseguire la simulazione.


if __name__ == "__main__":
    cli()  # Uso di click come punto di ingresso
				
			

Il metodo __call__ merita una spiegazione. Nelle classi Python esiste una raccolta di metodi double-underscore o ‘dunder’ che possono essere sovrascritti per generare un comportamento specifico. Uno di questi metodi è __call__. Se questo metodo viene sovrascritto, è possibile “chiamare” l’oggetto della classe istanziata come se fosse una funzione. Questo può essere molto potente e porta al concetto di oggetti funzione. Il metodo __call__ è usato per evitare di creare un metodo .run_class() o simile.

Ora che la struttura complessiva del codice è stata delineata è tempo di rivolgere l’attenzione ai singoli componenti.

Dettaglio codice

 

Importazioni

Il primo passo è dichiarare tutte le importazioni necessarie per questo script:

				
					import os
import random
import string

import click
import numpy as np
import pandas as pd
				
			

Tutte le importazioni in ordine alfabetico per ogni blocco, prima abbiamo il blocco delle librerie standard, poi le librerie esterne e infine librerie locali. In questo esempio non ci sono librerie locali, quindi dobbiamo considerare solo le sezioni della libreria standard e della libreria esterna. L’ordine alfabetico rende semplice vedere se una particolare libreria è stata importata o meno.

Dalla libreria standard importiamo os per interagire con il filesystem sull’output del CSV, random per garantire la piena riproducibilità in tutte le esecuzioni del codice e string per aiutare a generare simboli ticker casuali per i nomi dei file CSV.

Le librerie esterne includono click , NumPy e Pandas.

Click è usato per fornire un’interfaccia della riga di comando (CLI) in modo che ogni simulazione possa essere facilmente parametrizzata dalla riga di comando, piuttosto che richiedere valori hardcoded in file o configurazioni esterne. Ciò può essere utile, ad esempio, quando si utilizza il codice su un cluster HPC con il software di pianificazione delle attività Slurm .

NumPy è usato per eseguire la simulazione matematica vera e propria tramite la soluzione analitica del GBM SDE, mentre Pandas è usato per assemblare il DataFrame dei prezzi di apertura, massimo, minimo, chiusura e volume in modo che possa essere esportato in formato CSV sul disco.

La classe per il moto browniano geometrico

La classe GBM accetta molti parametri in modo da garantire una significativa flessibilità nelle simulazioni che si possono effettuare. Di seguito il codice per la definizione della classe e il metodo di inizializzazione. Come per tutti gli altri metodi, questo codice è stato ben documentato:

				
					
class GeometricBrownianMotionAssetSimulator:
    """
    Questa classe richiamabile genererà un DataFrame dei prezzi giornalieri di
    apertura-massimo-minimo-chiusura-volumi (OHLCV) per simulare i percorsi di
    prezzo delle azioni con il moto browniano geometrico per il prezzo e una
    distribuzione di Pareto per il volume.

    Produrrà i risultati in un CSV con un simbolo ticker generato casualmente.

    Per ora lo strumento è hardcoded per generare dati giornalieri dei
    giorni lavorativo tra due date, incluse.

    Si noti che i dati sui prezzi e sul volume sono completamente non correlati,
    il che non è probabile che si verifichi per i dati di asset reali.

    Parameters
    ----------
    start_date : `str`
        La data di inizio nel formato AAAA-MM-GG.
    end_date : `str`
        La data di fine nel formato AAAA-MM-GG.
    output_dir : `str`
         Il percorso completo della directory di output per il file CSV.
    symbol_length : `int`
        La lunghezza da usare per il simbolo ticker.
    init_price : `float`
        Il prezzo iniziale dell'asset.
    mu : `float`
        La "deriva" media dell'asset.
    sigma : `float`
        La "volatilità" dell'asset.
    pareto_shape : `float`
        Il parametro utilizzato per governare la forma di distribuzione
        di Pareto per la generazione dei dati di volume.
    """

    def __init__(
        self,
        start_date,
        end_date,
        output_dir,
        symbol_length,
        init_price,
        mu,
        sigma,
        pareto_shape
    ):
        self.start_date = start_date
        self.end_date = end_date
        self.output_dir = output_dir
        self.symbol_length = symbol_length
        self.init_price = init_price
        self.mu = mu
        self.sigma = sigma
        self.pareto_shape = pareto_shape
				
			

I commenti descrivono il significato di ogni parametro. In sostanza i parametri sono un set di date di inizio/fine, una directory di output in cui archiviare il file CSV, la lunghezza dei caratteri del simbolo ticker, nonché alcuni parametri statistici utilizzati per controllare la simulazione.

I parametri statistici includono la “deriva” e la “volatilità” della soluzione GBM, che possono essere modificate per generare titoli con una tendenza media più o meno al rialzo, nonché la loro volatilità. Si noti che questi valori sono costanti nel tempo. Cioè, il GBM non supporta la volatilità variabile nel tempo o “stocastica”. Questo sarà oggetto di successivi articoli.

Il parametro statistico finale è il pareto_shapeQuesto governa la forma della distribuzione di Pareto utilizzata per simulare il volume degli scambi giornalieri. Tecnicamente, all’aumentare di questo valore la distribuzione di Pareto si avvicina a una funzione delta di Dirac a zero. Cioè, un valore maggiore genererà probabilmente valori più estremi del volume degli scambi.

Il primo metodo all’interno della classe crea semplicemente una stringa ticker randomizzata, come JFEFX, per un dato numero di caratteri. Utilizza le librerie standard stringrandom per crearla in modo casuale dall’elenco di tutte le lettere  maiuscole ASCII:

				
					
    def _generate_random_symbol(self):
        """
        Genera una stringa di simbolo ticker casuale composta da caratteri
        ASCII maiuscoli da utilizzare nel nome file di output CSV.

        Returns
        -------
        `str`
            La stringa ticker casuale composta da lettere maiuscole.
        """
        return ''.join(
            random.choices(
                string.ascii_uppercase,
                k=self.symbol_length
            )
        )
				
			

Il metodo successivo utilizza Pandas per creare un DataFrame (inizialmente vuoto) di valori zero contenenti colonne per dateopenhighlowclosevolumeUtilizza il metodo date_range di Pandas per produrre una serie di giorni lavorativi tra le date di inizio e di fine, inclusi:

				
					
    def _create_empty_frame(self):
        """
        Crea il DataFrame Pandas vuoto con una colonna date
        utilizzando i giorni lavorativi tra due date. Ognuna
        delle colonne prezzo/volume è impostata su zero.

        Returns
        -------
        `pd.DataFrame`
            DataFrame OHLCV vuoto per il popolamento successivo.
        """
        date_range = pd.date_range(
            self.start_date,
            self.end_date,
            freq='B'
        )

        zeros = pd.Series(np.zeros(len(date_range)))

        return pd.DataFrame(
            {
                'date': date_range,
                'open': zeros,
                'high': zeros,
                'low': zeros,
                'close': zeros,
                'volume': zeros
            }
        )[['date', 'open', 'high', 'low', 'close', 'volume']]
				
			

Il metodo successivo è il nucleo della classe ed esegue effettivamente la simulazione dei possibili percorsi dei prezzi dell’asset. Questo metodo richiede alcune spiegazioni.

Innanzitutto è necessario determinare il tempo di fine in anni, dato dal valore T. Poiché ci sono circa 252 giorni lavorativi in ​​un anno e i dati sono giornalieri, T sarà (approssimativamente) uguale al numero di anni dei dati simulati.

Quindi è necessario calcolare dt, che è il timestep utilizzato per ogni successivo percorso dei prezzi dell’asset. Da notare che abbiamo applicato un fattore quattro. In questo modo è possibile simulare un percorso di asset con quattro volte più dati per tenere conto della necessità di simulare i valori di  quattro tipi di dati, cioè apertura, massimo, minimo e chiusura. In effetti, stiamo simulando quattro valori di prezzo per ogni giorno e  li analizziamo. Il calcolo di min/max per valori low/high è rimandato in un altro metodo.

Dopo aver calcolato la fase temporale, la formula corretta per il percorso dell’asset può essere applicato in modo vettorializzato. Questa formula è descritta in dettaglio in questo articolo .

Una volta che tutti i singoli valori up/down del percorso dell’asset sono stati simulati nella variabile asset_path, è necessario prendere il loro prodotto cumulativo e moltiplicare per un valore di prezzo iniziale per ottenere un percorso di prezzo realistico per, ad esempio, un’azione.

				
					    
    def _create_geometric_brownian_motion(self, data):
        """
        Calcola il percorso del prezzo di un asset utilizzando la
        soluzione analitica dell'equazione differenziale stocastica
        (SDE) del moto browniano geometrico.

        Questo divide il solito timestep per quattro in modo che la
        serie dei prezzi sia quattro volte più lunga, per tenere conto
        della necessità di avere un prezzo di apertura, massimo, minimo
         e chiusura per ogni giorno. Questi prezzi vengono successivamente
        delimitati correttamente in un ulteriore metodo.

        Parameters
        ----------
        data : `pd.DataFrame`
            Il DataFrame necessario per calcolare la lunghezza delle serie temporali.

        Returns
        -------
        `np.ndarray`
            Il percorso del prezzo dell'asset (quattro volte più lungo per includere OHLC).
        """
        n = len(data)
        T = n / 252.0  # Giorni lavorativi in un anno
        dt = T / (4.0 * n)  # 4.0 è necessario perchè sono richiesti quattro prezzi per ogni giorno

        # implementazione vettorializzata per la generazione di percorsi di asset
        # includendo quattro prezzo per ogni giorni, usati per creare OHLC
        asset_path = np.exp(
            (self.mu - self.sigma ** 2 / 2) * dt +
            self.sigma * np.random.normal(0, np.sqrt(dt), size=(4 * n))
        )

        return self.init_price * asset_path.cumprod()
				
			

Abbiamo detto che sono richiesti quattro prezzi per ogni giorno. Tuttavia, non vi era alcuna garanzia che i prezzi massimi e minimi simulati sarebbero stati effettivamente i prezzi massimi e minimi del giorno. Quindi il seguente metodo regola i prezzi massimo/minimo prendendo i valori massimo/minimo su tutti i valori del giorno (inclusi i prezzi di apertura/chiusura) in modo tale che la “barra” OHLC sia calcolata correttamente. La notazione di slicing NumPy viene utilizzata per scorrere in incrementi di quattro, eseguendo efficacemente i calcoli su base giornaliera:

				
					 
 
    def _append_path_to_data(self, data, path):
        """
        Tiene conto correttamente dei calcoli massimo/minimo necessari
        per generare un prezzo massimo e minimo corretto per il
        prezzo di un determinato giorno.

        Il prezzo di apertura prende ogni quarto valore, mentre il
        prezzo di chiusura prende ogni quarto valore sfalsato di 3
        (ultimo valore in ogni blocco di quattro).

        I prezzi massimo e minimo vengono calcolati prendendo il massimo
        (risp. minimo) di tutti e quattro i prezzi in un giorno e
        quindi aggiustando questi valori se necessario.

        Tutto questo viene eseguito sul posto in modo che il frame
        non venga restituito tramite il metodo.


        Parameters
        ----------
        data : `pd.DataFrame`
            Il DataFrame prezzo/volume da modificare sul posto.
        path : `np.ndarray`
            L'array NumPy originale del percorso del prezzo dell'asset.
        """
        data['open'] = path[0::4]
        data['close'] = path[3::4]

        data['high'] = np.maximum(
            np.maximum(path[0::4], path[1::4]),
            np.maximum(path[2::4], path[3::4])
        )

        data['low'] = np.minimum(
            np.minimum(path[0::4], path[1::4]),
            np.minimum(path[2::4], path[3::4])
        )
				
			

In questa fase il DataFrame ha ora le colonne open, high, low e close popolate. Tuttavia il volume deve ancora essere simulato. Il seguente metodo utilizza un campionamento vettorializzato di una distribuzione di Pareto per simulare il volume scambiato giornalmente per un titolo. L’ argomento shape della distribuzione controlla la dimensione dei valori restituiti, mentre il parametro size controlla il numero di estrazioni da effettuare. Questo è impostato per essere uguale al numero di giorni nel DataFrame.

Poiché la distribuzione di Pareto restituisce valori in virgola mobile, è necessario ridimensionarli e trasformarli in numeri interi per evitare il problema della simulazione di “quote frazionarie”. Anche i valori restituiti dalla distribuzione di Pareto vengono moltiplicati per un valore scalare di \(10^6\) per produrre valori di volume giornalieri tipici di azioni a grande capitalizzazione.

Si noti che non esiste alcuna autocorrelazione nei dati del volume, né è correlata al percorso dell’asset stesso. Si tratta di ipotesi semplificative utilizzate ai fini di questo articolo. Per un’implementazione più realistica sarebbe necessario aggiungere queste correlazioni nel modello:

				
					 
    def _append_volume_to_data(self, data):
        """
        Utilizza una distribuzione di Pareto per simulare dati di volume
        non negativi. Si noti che questo non è correlato al prezzo
        dell'attività sottostante, come sarebbe probabilmente il caso dei
        dati reali, ma è un'approssimazione ragionevolmente efficace.

        Parameters
        ----------
        data : `pd.DataFrame`
            Il DataFrame a cui aggiungere i dati del volume, sul posto.
        """
        data['volume'] = np.array(
            list(
                map(
                    int,
                    np.random.pareto(
                        self.pareto_shape,
                        size=len(data)
                    ) * 1000000.0
                )
            )
        )
				
			

Il metodo successivo memorizza semplicemente il file CSV su disco assicurandosi di visualizzare solo due cifre decimali per le informazioni sui prezzi in virgola mobile:

				
					
    def _output_frame_to_dir(self, symbol, data):
        """
        Output the fully-populated DataFrame to disk into the
        desired output directory, ensuring to trim all pricing
        values to two decimal places.
        Memorizza il DataFrame completamente popolato su disco
        nella directory di output desiderata, assicurandosi di
        ridurre tutti i valori dei prezzi a due cifre decimali.

        Parameters
        ----------
        symbol : `str`
            Il simbolo ticker con cui denominare il file.
        data : `pd.DataFrame`
            DataFrame contenente i dati OHLCV generati.
        """
        output_file = os.path.join(self.output_dir, '%s.csv' % symbol)
        data.to_csv(output_file, index=False, float_format='%.2f')
				
			

Il metodo __call__ è la classe ‘entrypoint’. Come si può vedere, assembla semplicemente tutti i metodi descritti in precedenza e li esegue in sequenza. Si tratta di un approccio di “codice pulito” ampiamente utilizzato che assicura che sia semplice ispezionare la metodologia complessiva di elaborazione dei dati della classe:

				
					
    def __call__(self):
        """
        Il punto di ingresso per la generazione del frame OHLCV dell'asset. Si genera
        un simbolo e un dataframe vuoto. Quindi popola questo dataframe con alcuni
        dati GBM simulati. Il volume dell'asset viene quindi aggiunto a questi
        dati e infine viene salvato su disco come CSV.
        """
        symbol = self._generate_random_symbol()
        data = self._create_empty_frame()
        path = self._create_geometric_brownian_motion(data)
        self._append_path_to_data(data, path)
        self._append_volume_to_data(data)
        self._output_frame_to_dir(symbol, data)
				
			

In questa fase l’esecuzione del file non farà nulla poiché la classe non è stata ancora istanziata o eseguita. Questo è il compito della seguente funzione cli() che utilizza la libreria Click.

Punto di ingresso con Click

La libreria Click consente di specificare un insieme di parametri dalla riga di comando, con valori predefiniti facoltativi e descrizioni dell’helper. Lo schema standard prevede di definire una funzione chiamata cli che accetta una insieme di nomi di parametri. Questi nomi corrispondono ai parametri generati con ciascuno dei decoratori @click.option(...) specificando quale parametro è mappato con un’opzione della riga di comando viene mappata e in quale modalità.

Da notare che l’insieme  di parametri forniti da Click è quasi identica a quelli inseriti nella classe. Questa è una tecnica molto potente in quanto consente in modo efficace di esporre i parametri della classe tramite una CLI, che può quindi essere ulteriormente utilizzata come parte di un processo di dati più ampio.

Il vantaggio di disaccoppiare la classe dal punto di ingresso Click è la possibilità di importare la classe all’interno di altri moduli Python senza la necessità di essere eseguira tramite Click. Tuttavia, è presente anche la capacità di eseguire la classe tramite  CLI, nel caso sia la modalità desiderata. L’esecuzione di questo modello su molte classi consente un riutilizzo significativo tra gli strumenti, risparmiando un notevole sforzo per gli sviluppi futuri.

Tutti i parametri sono stati descritti precedentemente nel metodo di inizializzazione della classe, ad eccezione di num_assets, che è un valore intero utilizzato per controllare quanti file CSV separati devono essere generati. Cioè, quanti titoli separati simulare.

Una volta che i parametri sono stati opportunamente convertiti nei tipi corretti, impostiamo i valori del seed casuale di NumPy in modo da garantire che i dati siano completamente riproducibili a parità di seed fornito. In altre parole, i risultati dovrebbero corrispondere esattamente a quelli riportati di seguito se si usa lo stesso seed e gli stessi parametri.

Infine la classe viene istanziata e viene chiamata (utilizzando il metodo __call__ descritto sopra) per ogni numero desiderato di asset. Ciò si ottiene chiamando la classe come se fosse una funzione (vedere la seguente riga gbmas()):

				
					
@click.command()
@click.option('--num-assets', 'num_assets', default='1', help='Numero di asset separati per cui generare file')
@click.option('--random-seed', 'random_seed', default='42', help='Seed casuale da impostare sia per Python che per NumPy per la riproducibilità')
@click.option('--start-date', 'start_date', default=None, help='La data di inizio per la generazione dei dati sintetici nel formato AAAA-MM-GG')
@click.option('--end-date', 'end_date', default=None, help='La data di inizio per la generazione dei dati sintetici nel formato AAAA-MM-GG')
@click.option('--output-dir', 'output_dir', default=None, help='La posizione in cui inviare il file CSV di dati sintetici')
@click.option('--symbol-length', 'symbol_length', default='5', help='La lunghezza del simbolo dell''asset utilizzando caratteri ASCII maiuscoli')
@click.option('--init-price', 'init_price', default='100.0', help='Il prezzo iniziale da utilizzare')
@click.option('--mu', 'mu', default='0.1', help='Il parametro di deriva, \mu per GBM SDE')
@click.option('--sigma', 'sigma', default='0.3', help='Il parametro di volatilità, \sigma per SDE GBM')
@click.option('--pareto-shape', 'pareto_shape', default='1.5', help='La forma della distribuzione di Pareto che simula il volume degli scambi')
def cli(num_assets, random_seed, start_date, end_date, output_dir, symbol_length, init_price, mu, sigma, pareto_shape):
    num_assets = int(num_assets)
    random_seed = int(random_seed)
    symbol_length = int(symbol_length)
    init_price = float(init_price)
    mu = float(mu)
    sigma = float(sigma)
    pareto_shape = float(pareto_shape)

    # Seed per Python e NumPy
    random.seed(random_seed)
    np.random.seed(seed=random_seed)

    gbmas = GeometricBrownianMotionAssetSimulator(
        start_date,
        end_date,
        output_dir,
        symbol_length,
        init_price,
        mu,
        sigma,
        pareto_shape
    )

    # Crea num_assets file tramite la chiamata 
    # ripetuta alla classe istanziata
    for i in range(num_assets):
        print('Generating asset path %d of %d...' % (i+1, num_assets))
        gbmas()
				
			

Infine, per eseguire lo script è necessario creare un’istruzione if __name__ == "__main__": per dire effettivamente all’interprete Python di eseguire la  funzione cli():

				
					if __name__ == "__main__":
    cli()
				
			

Questo completa la descrizione del codice.

Esecuzione del codice

Il codice è ora eseguibile dalla riga di comando. Tuttavia, deve ancora essere eseguito all’interno di un ambiente Python adatto che contenga tutte le dipendenze esterne (Click, NumPy e Pandas). Il modo più semplice per raggiungere questo obiettivo è installare la distribuzione Anaconda Python disponibile gratuitamente su una macchina locale. Inoltre potrebbe essere necessario installare Click. È quindi possibile eseguire il codice aprendo un terminale della riga di comando ed eseguendo quanto segue:

				
					python gbm.py --num-assets=5 --random-seed=41 --start-date='1993-01-01' --end-date='2017-12-31' --output-dir='.' --symbol-lenth=5 --init-price=100.0 --mu=0.1 --sigma=0.3 --pareto-shape=1.5
				
			

Con questo comando generiamo cinque file CSV separati nella stessa directory della posizione dello script che copre il periodo dall’inizio del 1993 fino alla fine del 2017, con vari valori utilizzati per le distribuzioni statistiche.

I grafici dei dati

Come controllo finale vale la pena caricare i dati tramite Pandas e visualizzarli tramite Matplotib. Un semplice script per raggiungere questo obiettivo è il seguente:

				
					import matplotlib.pyplot as plt
import pandas as pd


if __name__ == "__main__":
    # Cambiare BUAWV.csv nel simbolo del ticker desiderato
    df = pd.read_csv('BUAWV.csv').set_index('date')
    df[['open', 'high', 'low', 'close']].plot()
    plt.show()
				
			

Questo produce un grafico simle al seguente:

trading-algoritmico-gbm-output

Il percorso dell’asset e il comportamento del volume (con un multiplo di volume molto ridotto) possono essere visti chiaramente.

Prossimi passi

Sebbene la combinazione del Geometric Brownian Motion SDE e della distribuzione di Pareto sia un modello sufficiente per produrre dati sul percorso degli asset, manca di sofisticazione rispetto ai dati sulle azioni reali. In particolare, non è in grado di tenere conto dell’inversione alla media, della volatilità stocastica, dei volumi/prezzi autocorrelati e di altri comportamenti di serie temporali più complessi. In articoli futuri la classe precedente verrà modificata ed estesa per gestire ulteriori modelli di serie temporali.

Inoltre descriveremo come eseguire tale strumento su un cluster Raspberry Pi, al fine di creare una significativa libreria di dati sintetici che può essere utilizzata per scopi di sviluppo del modello di backtest.

Codice completo

				
					import os
import random
import string

import click
import numpy as np
import pandas as pd


class GeometricBrownianMotionAssetSimulator:
    """
    Questa classe richiamabile genererà un DataFrame dei prezzi giornalieri di
    apertura-massimo-minimo-chiusura-volumi (OHLCV) per simulare i percorsi di
    prezzo delle azioni con il moto browniano geometrico per il prezzo e una
    distribuzione di Pareto per il volume.

    Produrrà i risultati in un CSV con un simbolo ticker generato casualmente.

    Per ora lo strumento è hardcoded per generare dati giornalieri dei
    giorni lavorativo tra due date, incluse.

    Si noti che i dati sui prezzi e sul volume sono completamente non correlati,
    il che non è probabile che si verifichi per i dati di asset reali.

    Parameters
    ----------
    start_date : `str`
        La data di inizio nel formato AAAA-MM-GG.
    end_date : `str`
        La data di fine nel formato AAAA-MM-GG.
    output_dir : `str`
         Il percorso completo della directory di output per il file CSV.
    symbol_length : `int`
        La lunghezza da usare per il simbolo ticker.
    init_price : `float`
        Il prezzo iniziale dell'asset.
    mu : `float`
        La "deriva" media dell'asset.
    sigma : `float`
        La "volatilità" dell'asset.
    pareto_shape : `float`
        Il parametro utilizzato per governare la forma di distribuzione
        di Pareto per la generazione dei dati di volume.
    """

    def __init__(
        self,
        start_date,
        end_date,
        output_dir,
        symbol_length,
        init_price,
        mu,
        sigma,
        pareto_shape
    ):
        self.start_date = start_date
        self.end_date = end_date
        self.output_dir = output_dir
        self.symbol_length = symbol_length
        self.init_price = init_price
        self.mu = mu
        self.sigma = sigma
        self.pareto_shape = pareto_shape

    def _generate_random_symbol(self):
        """
        Genera una stringa di simbolo ticker casuale composta da caratteri
        ASCII maiuscoli da utilizzare nel nome file di output CSV.

        Returns
        -------
        `str`
            La stringa ticker casuale composta da lettere maiuscole.
        """
        return ''.join(
            random.choices(
                string.ascii_uppercase,
                k=self.symbol_length
            )
        )

    def _create_empty_frame(self):
        """
        Crea il DataFrame Pandas vuoto con una colonna date
        utilizzando i giorni lavorativi tra due date. Ognuna
        delle colonne prezzo/volume è impostata su zero.

        Returns
        -------
        `pd.DataFrame`
            DataFrame OHLCV vuoto per il popolamento successivo.
        """
        date_range = pd.date_range(
            self.start_date,
            self.end_date,
            freq='B'
        )

        zeros = pd.Series(np.zeros(len(date_range)))

        return pd.DataFrame(
            {
                'date': date_range,
                'open': zeros,
                'high': zeros,
                'low': zeros,
                'close': zeros,
                'volume': zeros
            }
        )[['date', 'open', 'high', 'low', 'close', 'volume']]

    def _create_geometric_brownian_motion(self, data):
        """
        Calcola il percorso del prezzo di un asset utilizzando la
        soluzione analitica dell'equazione differenziale stocastica
        (SDE) del moto browniano geometrico.

        Questo divide il solito timestep per quattro in modo che la
        serie dei prezzi sia quattro volte più lunga, per tenere conto
        della necessità di avere un prezzo di apertura, massimo, minimo
         e chiusura per ogni giorno. Questi prezzi vengono successivamente
        delimitati correttamente in un ulteriore metodo.

        Parameters
        ----------
        data : `pd.DataFrame`
            Il DataFrame necessario per calcolare la lunghezza delle serie temporali.

        Returns
        -------
        `np.ndarray`
            Il percorso del prezzo dell'asset (quattro volte più lungo per includere OHLC).
        """
        n = len(data)
        T = n / 252.0  # Giorni lavorativi in un anno
        dt = T / (4.0 * n)  # 4.0 è necessario perchè sono richiesti quattro prezzi per ogni giorno

        # implementazione vettorializzata per la generazione di percorsi di asset
        # includendo quattro prezzo per ogni giorni, usati per creare OHLC
        asset_path = np.exp(
            (self.mu - self.sigma ** 2 / 2) * dt +
            self.sigma * np.random.normal(0, np.sqrt(dt), size=(4 * n))
        )

        return self.init_price * asset_path.cumprod()

    def _append_path_to_data(self, data, path):
        """
        Tiene conto correttamente dei calcoli massimo/minimo necessari
        per generare un prezzo massimo e minimo corretto per il
        prezzo di un determinato giorno.

        Il prezzo di apertura prende ogni quarto valore, mentre il
        prezzo di chiusura prende ogni quarto valore sfalsato di 3
        (ultimo valore in ogni blocco di quattro).

        I prezzi massimo e minimo vengono calcolati prendendo il massimo
        (risp. minimo) di tutti e quattro i prezzi in un giorno e
        quindi aggiustando questi valori se necessario.

        Tutto questo viene eseguito sul posto in modo che il frame
        non venga restituito tramite il metodo.


        Parameters
        ----------
        data : `pd.DataFrame`
            Il DataFrame prezzo/volume da modificare sul posto.
        path : `np.ndarray`
            L'array NumPy originale del percorso del prezzo dell'asset.
        """
        data['open'] = path[0::4]
        data['close'] = path[3::4]

        data['high'] = np.maximum(
            np.maximum(path[0::4], path[1::4]),
            np.maximum(path[2::4], path[3::4])
        )

        data['low'] = np.minimum(
            np.minimum(path[0::4], path[1::4]),
            np.minimum(path[2::4], path[3::4])
        )

    def _append_volume_to_data(self, data):
        """
        Utilizza una distribuzione di Pareto per simulare dati di volume
        non negativi. Si noti che questo non è correlato al prezzo
        dell'attività sottostante, come sarebbe probabilmente il caso dei
        dati reali, ma è un'approssimazione ragionevolmente efficace.

        Parameters
        ----------
        data : `pd.DataFrame`
            Il DataFrame a cui aggiungere i dati del volume, sul posto.
        """
        data['volume'] = np.array(
            list(
                map(
                    int,
                    np.random.pareto(
                        self.pareto_shape,
                        size=len(data)
                    ) * 1000000.0
                )
            )
        )

    def _output_frame_to_dir(self, symbol, data):
        """
        Output the fully-populated DataFrame to disk into the
        desired output directory, ensuring to trim all pricing
        values to two decimal places.
        Memorizza il DataFrame completamente popolato su disco
        nella directory di output desiderata, assicurandosi di
        ridurre tutti i valori dei prezzi a due cifre decimali.

        Parameters
        ----------
        symbol : `str`
            Il simbolo ticker con cui denominare il file.
        data : `pd.DataFrame`
            DataFrame contenente i dati OHLCV generati.
        """
        output_file = os.path.join(self.output_dir, '%s.csv' % symbol)
        data.to_csv(output_file, index=False, float_format='%.2f')

    def __call__(self):
        """
        Il punto di ingresso per la generazione del frame OHLCV dell'asset. Si genera
        un simbolo e un dataframe vuoto. Quindi popola questo dataframe con alcuni
        dati GBM simulati. Il volume dell'asset viene quindi aggiunto a questi
        dati e infine viene salvato su disco come CSV.
        """
        symbol = self._generate_random_symbol()
        data = self._create_empty_frame()
        path = self._create_geometric_brownian_motion(data)
        self._append_path_to_data(data, path)
        self._append_volume_to_data(data)
        self._output_frame_to_dir(symbol, data)


@click.command()
@click.option('--num-assets', 'num_assets', default='1', help='Numero di asset separati per cui generare file')
@click.option('--random-seed', 'random_seed', default='42', help='Seed casuale da impostare sia per Python che per NumPy per la riproducibilità')
@click.option('--start-date', 'start_date', default=None, help='La data di inizio per la generazione dei dati sintetici nel formato AAAA-MM-GG')
@click.option('--end-date', 'end_date', default=None, help='La data di inizio per la generazione dei dati sintetici nel formato AAAA-MM-GG')
@click.option('--output-dir', 'output_dir', default=None, help='La posizione in cui inviare il file CSV di dati sintetici')
@click.option('--symbol-length', 'symbol_length', default='5', help='La lunghezza del simbolo dell''asset utilizzando caratteri ASCII maiuscoli')
@click.option('--init-price', 'init_price', default='100.0', help='Il prezzo iniziale da utilizzare')
@click.option('--mu', 'mu', default='0.1', help='Il parametro di deriva, \mu per GBM SDE')
@click.option('--sigma', 'sigma', default='0.3', help='Il parametro di volatilità, \sigma per SDE GBM')
@click.option('--pareto-shape', 'pareto_shape', default='1.5', help='La forma della distribuzione di Pareto che simula il volume degli scambi')
def cli(num_assets, random_seed, start_date, end_date, output_dir, symbol_length, init_price, mu, sigma, pareto_shape):
    num_assets = int(num_assets)
    random_seed = int(random_seed)
    symbol_length = int(symbol_length)
    init_price = float(init_price)
    mu = float(mu)
    sigma = float(sigma)
    pareto_shape = float(pareto_shape)

    # Seed per Python e NumPy
    random.seed(random_seed)
    np.random.seed(seed=random_seed)

    gbmas = GeometricBrownianMotionAssetSimulator(
        start_date,
        end_date,
        output_dir,
        symbol_length,
        init_price,
        mu,
        sigma,
        pareto_shape
    )

    # Crea num_assets file tramite la chiamata
    # ripetuta alla classe istanziata
    for i in range(num_assets):
        print('Generating asset path %d of %d...' % (i+1, num_assets))
        gbmas()


if __name__ == "__main__":
    cli()
				
			

Gli altri articoli di questa serie

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

Torna in alto
Scroll to Top