Creare un set di dati dell'S&P500 senza il bias di sopravvivenza in Python

Creare un set di dati per l’S&P500 senza il bias di sopravvivenza in Python

Sommario

In questo articolo descriviamo come creare un set di dati per l’S&P500 senza il bias di sopravvivenza in Python per migliorare il backtest di strategie di trading algoritmico e quantitativo.

Quando si sviluppa una strategia di trading azionario, è importante che il backtest sia il più accurato possibile. In alcune delle strategie  descritte nei precedenti articoli, abbiamo notato che il backtest non teneva conto del bias di sopravvivenza. Il survivorship bias (o bias di sopravvivenza) è una forma di bias di selezione causata dal concentrarsi solo su asset che hanno già superato una sorta di processo selettivo.

Un semplice esempio è una strategia che semplicemente acquista e mantiene un’equa allocazione degli attuali componenti dell’S&P500.

Il dataset dell’S&P500

Possiamo utilizzare i dati S&P 500 di un articolo precedente. Possiamo quindi confrontare la performance di questa strategia con le performance di RSP, un ETF che replica l’indice S&P 500 Equal Weight.

				
					import matplotlib.pyplot as plt
import pandas as pd
import yfinance as yf

tickers = pd.read_csv('spy/tickers.csv', header=None)[1].tolist()

# calcola il prodotto cumulativo della media di tutti i rendimenti giornalieri
# ovvero simulare una crescita di $1 pesando equamente tutti gli attuali 
# componenti dell'indice S&P 500
sim_rsp = (
    (pd.concat(
        [pd.read_csv(f"spy/{ticker}.csv", index_col='date', parse_dates=True)[
            'close'
        ].pct_change()
        for ticker in tickers],
        axis=1,
        sort=True,
    ).mean(axis=1, skipna=True) + 1)
    .cumprod()
    .rename("SIM")
)

# scarida i dati di RSP
rsp = (
    (yf.download("RSP", sim_rsp.index[0], sim_rsp.index[-1])[
        "Adj Close"
    ].pct_change() + 0.002 / 252 + 1)  # ER annuale del 0.20%
    .cumprod()
    .rename("RSP")
)

sim_rsp.plot(legend=True, title="RSP vs. Survivorship-Biased Strategy", figsize=(12, 9))
rsp.plot(legend=True)
plt.show()
				
			
Creare un set di dati dell'S&P500 senza il bias di sopravvivenza in Python

Se avessimo iniziato a fare trading con questa strategia nel 2014 avremmo facilmente battuto il mercato. Dobbiamo solo conoscere i componenti dell’S&P 500 tra 5 anni… Questo semplice esempio mostra come un trader possa essere indotto con l’inganno a pensare di avere una buona strategia perché non ha tenuto conto del survival bias.

Dati privi del bias di sopravvivenza

Come possiamo creare prevenire il bias di sopravvivenza? È più facile a dirsi che a farsi. Per creare un set di dati per l’S&P500 senza il bias di sopravvivenza, dobbiamo prima conoscere tutti i componenti storici dell’S&P 500 nell’arco temporale desiderato. Una volta che conosciamo tutti i componenti, possiamo mettere insieme un set di dati utilizzando i dati storici sui prezzi di tutti i componenti durante il periodo di tempo in cui erano nell’S&P 500.

Sfortunatamente ci sono alcuni ostacoli che rendono la raccolta dei dati più difficile di quanto ci si potrebbe aspettare. Innanzitutto, è difficile ottenere dati storici sui componenti dell’indice S&P 500. I componenti attuali possono essere recuperati da Wikipedia, ma trovare componenti storici è quasi impossibile senza acquistare i dati. In secondo luogo, una volta che si conoscono i componenti storici, anche trovare dati sui prezzi può essere difficile. Le società dell’indice S&P 500 vengono periodicamente rinominate, acquisite e alcune addirittura falliscono. Le fonti di dati gratuite come Yahoo Finance di solito non dispongono di dati sui titoli cancellati; Anche i cambiamenti storici dei nomi dei ticker non sono ben documentati.

Origine dei dati

 

Costituenti

Anche se non disponiamo di dati storici reali sui componenti, possiamo fare una buona approssimazione utilizzando il portafoglio di un ETF che segue l’indice S&P 500. L’ETF iShares Core S&P 500 (IVV) rende pubbliche le sue partecipazioni ogni mese e dichiara di investire almeno il 90% delle sue attività in titoli dell’indice. Con un po’ di web scraping possiamo facilmente ottenere le sue partecipazioni mensili dal 2006.

				
					
import requests
from bs4 import BeautifulSoup
from datetime import datetime, timedelta
import json

# request di una pagina
html = requests.get("https://www.ishares.com/us/products/239726/#tabsAll").content
soup = BeautifulSoup(html)

# cerca le date disponibili
holdings = soup.find("div", {"id": "holdings"})
dates_div = holdings.find_all("div", "component-date-list")[1]
dates_div.find_all("option")
dates = [option.attrs["value"] for option in dates_div.find_all("option")]

# scarica i costituenti per ogni data
constituents = pd.Series()
for date in dates:
    resp = requests.get(
        f"https://www.ishares.com/us/products/239726/ishares-core-sp-500-etf/1467271812596.ajax?tab=all&fileType=json&asOfDate={date}"
    ).content[3:]
    tickers = json.loads(resp)
    tickers = [(arr[0], arr[1]) for arr in tickers['aaData']]
    date = datetime.strptime(date, "%Y%m%d")
    constituents[date] = tickers

constituents = constituents.iloc[::-1] # inverte in ordine cronologico
constituents.head()
				
			
				
					2006-09-29    [(PMCS, PMC-SIERRA INC.), (ANDW, ANDREW CORP.)...
2006-10-31    [(PMCS, PMC-SIERRA INC.), (PGL, PEOPLES ENERGY...
2006-11-30    [(PMCS, PMC-SIERRA INC.), (ADCT, ADC TELECOMMU...
2006-12-29    [(PMCS, PMC-SIERRA INC.), (ADCT, ADC TELECOMMU...
2007-01-31    [(PMCS, PMC-SIERRA INC.), (PGL, PEOPLES ENERGY...
dtype: object
				
			

Come possiamo vedere, per ogni mese abbiamo una lista di tutti i ticker e i nomi delle società detenuti nell’ETF. Possiamo ora scaricare i dati  storici dei prezzi.

Dati sui prezzi

Come accennato in precedenza, recuperare i dati storici per tutti i componenti dell’indice può essere difficile. Fortunatamente per noi, il set di dati dei prezzi WIKI di Quandl contiene la maggior parte dei dati di cui abbiamo bisogno. Sebbene il feed di dati non sia più aggiornato, contiene ancora dati accurati precedenti ad aprile 2018. Possiamo esportare i dati dopo aver creato un account gratuito. Dopo aver scaricato  i dati li possiamo separare per ogni titolo.

				
					
wiki = pd.read_csv("WIKI_PRICES.csv", parse_dates=True)
wiki = dict(tuple(wiki.groupby('ticker')))
for ticker in wiki:
    wiki[ticker].set_index("date", inplace=True)
				
			

Sebbene il set di dati WIKI contenga la maggior parte dei dati di cui abbiamo bisogno, non li ha tutti. Usiamo Yahoo Finance per scaricare i dati rimanenti. Scriviamo alcune funzioni di supporto per aiutarci a scaricare i dati.

				
					
import time
import re

def quandl_data(ticker, start, end):
    if ticker in wiki:
        df = wiki[ticker][start:end]
    else:
        ticker = fix_ticker(ticker)
        if ticker in wiki:
            df = wiki[ticker][start:end]
        else:
            return None
    df = df.drop(['open','high','low','close', 'volume','ex-dividend','split_ratio', 'ticker'], axis=1)
    df = df.rename(index=str, columns={"adj_open": "open",
                                       "adj_high": "high",
                                       "adj_low": "low",
                                       "adj_close": "close",
                                       "adj_volume": "volume"})
    return df

def yahoo_data(ticker, start, end):
    ticker = fix_ticker(ticker)
    try:
        df =yf.download(ticker, start, end)
    except:
        time.sleep(1)
        try:
            df = yf.download(ticker, start, end)
        except:
            return None
    # aggiustare i dati ohlc usando adj close
    adjfactor = df["Close"] / df["Adj Close"]
    df["Open"] /= adjfactor
    df["High"] /= adjfactor
    df["Low"] /= adjfactor
    df["Close"] = df["Adj Close"]
    df["Volume"] *= adjfactor
    df = df.drop(["Adj Close"], axis=1)
    df = df.rename(str.lower, axis='columns')
    df.index.rename('date', inplace=True)
    return df

def fix_ticker(ticker):
    rename_table = {
        "-": "LPRAX", # BlackRock LifePath Dynamic Retirement Fund
        "8686": "AFL", # AFLAC
        "4XS": "ESRX", # Express Scripts Holding Company 
        "AAZ": "APC", # Anadarko Petroleum Corporation
        "AG4": "AGN", # Allergan plc
        "BFB": "BF_B", # Brown-Forman Corporation
        "BF.B": "BF_B", # Brown-Forman Corporation
        "BF/B": "BF_B", # Brown-Forman Corporation
        "BLD WI": "BLD", # TopBuild Corp.
        "BRKB": "BRK_B", # Berkshire Hathaway Inc.
        "CC WI": "CC", # The Chemours Company
        "DC7": "DFS", # Discover Financial Services
        "GGQ7": "GOOG", # Alphabet Inc. Class C
        "HNZ": "KHC", # The Kraft Heinz Company
        "LOM": "LMT", # Lockheed Martin Corp.
        "LTD": "LB", # L Brands Inc.
        "LTR": "L", # Loews Corporation
        "MPN": "MPC", # Marathon Petroleum Corp.
        "MWZ": "MET", # Metlife Inc.
        "MX4A": "CME", # CME Group Inc.
        "NCRA": "NWSA", # News Corporation
        "NTH": "NOC", # Northrop Grumman Crop.
        "PA9": "TRV", # The Travelers Companies, Inc.
        "QCI": "QCOM", # Qualcomm Inc.
        "RN7": "RF", # Regions Financial Corp
        "SLBA": "SLB", # Schlumberger Limited
        "SYF-W": "SYF", # Synchrony Financial
        "SWG": "SCHW", # The Charles Schwab Corporation 
        "UAC/C": "UAA", # Under Armour Inc Class A
        "UBSFT": "UBSFY", # Ubisoft Entertainment
        "USX1": "X", # United States Steel Corporation
        "UUM": "UNM", # Unum Group
        "VISA": "V", # Visa Inc 
    }
    if ticker in rename_table:
        fix = rename_table[ticker]
    else:
        fix = re.sub(r'[^A-Z]+', '', ticker)
    return fix
				
			

Abbiamo creato un grande dizionario rename_table. Come accennato in precedenza, le aziende possono cambiare i ticker dei loro titoli azionari e spesso non è ben documentato. Abbiamo creato manualmente  una mappa dal vecchio al nuovo simbolo del ticker. Questo approccio aggiunge una componente di errore umano.

Compilazione dei dati

Ora siamo pronti per compilare tutti i dati  usando la lista dei componenti! Memorizziamo anche le azioni con dati mancanti nella lista skips.

				
					
data = {}
skips = set()

constituents = constituents['2013-02-28':'2018-02-28']

for i in range(0, len(constituents) - 1):
    start = str(constituents.index[i].date())
    end = str((constituents.index[i + 1].to_pydatetime() - timedelta(days=1)).date())
    for company in constituents[i]:
        if company in skips:
            continue
        df = quandl_data(company[0], start, end)
        if df is None:
            df = yahoo_data(company[0], start, end)
        if df is None:
            skips.add(company)
            continue
        if company[0] in data:
            data[company[0]] = data[company[0]].append(df)
        else:
            data[company[0]] = df
				
			

Otteniamo i seguenti dati all’interno della lista skips.

				
					
{('ACT', 'ACTAVIS INC.'),
 ('ACT', 'ACTAVIS PLC'),
 ('BLKFDS', 'BLK CSH FND TREASURY SL AGENCY'),
 ('BMC', 'BMC SOFTWARE INC.'),
 ('CVH', 'COVENTRY HEALTH CARE INC'),
 ('ESH5', 'S&P500 EMINI MAR 15'),
 ('ESH6', 'S&P500 EMINI MAR 16'),
 ('ESH7', 'S&P500 EMINI MAR 17'),
 ('ESH8', 'S&P500 EMINI MAR 18'),
 ('ESM5', 'S&P500 EMINI JUN 15'),
 ('ESM6', 'S&P500 EMINI JUN 16'),
 ('ESM7', 'S&P500 EMINI JUN 17'),
 ('ESU5', 'S&P500 EMINI SEP 15'),
 ('ESU6', 'S&P500 EMINI SEP 16'),
 ('ESU7', 'S&P500 EMINI SEP 17'),
 ('ESZ4', 'S&P500 EMINI DEC 14'),
 ('ESZ5', 'S&P500 EMINI DEC 15'),
 ('ESZ6', 'S&P500 EMINI DEC 16'),
 ('ESZ7', 'S&P500 EMINI DEC 17'),
 ('HAWKB', 'BLACKHAWK NETWORK HOLDINGS INC CLA'),
 ('MARGIN_USD', 'FUTURES USD MARGIN BALANCE'),
 ('MOLX', 'MOLEX INC.'),
 ('NYX', 'NYSE EURONEXT'),
 ('PCS', 'METROPCS COMMUNICATIONS INC.'),
 ('UBFUT', 'CASH COLLATERAL USD UBFUT')}
				
			

Come possiamo vedere, ci sono alcune società per le quali non siamo riusciti a ottenere dati perché nel frattempo sono state acquisite. Potrebbe essere possibile trovare i dati da una fonte diversa, ma manteniamo ciò che abbiamo finora. Inoltre, notiamo che l’ETF deteneva occasionalmente futures S&P 500 e garanzie collaterali in contanti, che fanno parte di meno del 10% delle partecipazioni non incluse nell’indice.

Esportazione

Ora possiamo esportare nuovamente i nostri dati in CSV.

				
					
for ticker, df in data.items():
    df = df.reset_index().drop_duplicates(subset='date').set_index('date')
    df.to_csv(f"survivorship-free/{fix_ticker(ticker)}.csv")
    data[ticker] = df
    
tickers = [fix_ticker(ticker) for ticker in data.keys()]
pd.Series(tickers).to_csv("survivorship-free/tickers.csv")
				
			

Test

Ora che disponiamo di dati privi del bias di sopravvivenza, testiamoli rispetto all’RSP, l’ETF dell’S&P 500 equi-pesato usato in precedenza.

				
					
sim_rsp = (
    (pd.concat(
        [pd.read_csv(f"survivorship-free/{ticker}.csv", index_col='date', parse_dates=True)[
            'close'
        ].pct_change()
        for ticker in tickers],
        axis=1,
        sort=True,
    ).mean(axis=1, skipna=True) + 1)
    .cumprod()
    .rename("SIM")
)

rsp = (
    (web.DataReader("RSP", "yahoo", sim_rsp.index[0], sim_rsp.index[-1])[
        "Adj Close"
    ].pct_change() + 0.002 / 252 + 1)  # 0.20% annual ER
    .cumprod()
    .rename("RSP")
)

sim_rsp.plot(legend=True, title="RSP vs. Un-Survivorship-Biased Strategy", figsize=(12, 9))
rsp.plot(legend=True);
				
			
Survivorship-bias-sp500-dataset-RSP-grafico

Le linee sono quasi identiche! Sebbene il nostro set di dati non sia storicamente accurato al 100%, è sicuramente un approccio migliore rispetto al semplice utilizzo degli attuali componenti dell’S&P 500. Ora che disponiamo di un set di dati privo del bias di sopravvivenza, possiamo testare le future strategie con maggiore precisione.

Codice completo

In questo articolo abbiamo descritto come creare un set di dati per l’S&P500 senza il bias di sopravvivenza in Python. Per il codice completo riportato in questo articolo, si può consultare il seguente repository di github:
https://github.com/datatrading-info/AnalisiDatiFinanziari

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