[Minería de datos] Introducción de la tecnología NLP al análisis del mercado de valores

1. Descripción

        Los modelos de aprendizaje automático implementados en el comercio generalmente se entrenan en los precios históricos de las acciones y otros datos cuantitativos para predecir los precios futuros de las acciones. Sin embargo, el procesamiento del lenguaje natural (NLP) nos permite analizar documentos financieros, como el Formulario 10-k, para predecir los movimientos de las existencias.

2. Interpretación del procesamiento del lenguaje natural

Crédito de la imagen: Adam Gaitgay

        El procesamiento del lenguaje natural es una rama de la inteligencia artificial que implica enseñar a las computadoras a leer el lenguaje y extraer significado de él. Debido a que el lenguaje es tan complejo, las computadoras deben pasar por una serie de pasos para comprender el texto. A continuación se muestra una descripción rápida de los pasos que ocurren en una canalización típica de NLP.

  1. Segmentación de oraciones Los documentos de texto se segmentan
    en oraciones individuales.
  2. Tokenización
    Una vez que el documento se divide en oraciones, dividimos aún más las oraciones en palabras individuales. Cada palabra se llama token, de ahí el nombre de tokenización.
  3. Etiquetado de partes del discurso
    Alimentamos cada etiqueta y algunas palabras a su alrededor en un modelo de clasificación de partes del discurso previamente entrenado para recibir la parte del discurso etiquetada como salida.
  4. Lematización
    Las palabras a menudo aparecen en diferentes formas al referirse al mismo objeto/acción. Para evitar que las computadoras traten las diferentes formas de una palabra como palabras diferentes, realizamos la lematización, el proceso de combinar varias inflexiones de palabras para analizarlas como un solo elemento, determinado por el logotipo del lema de la palabra (la posición de la palabra en un diccionario Apariencia).
  5. Palabras vacías Las palabras extremadamente comunes como "y", "el" y "un" no proporcionan ningún valor, por lo que las identificamos como palabras vacías para
    excluirlas de cualquier análisis realizado en el texto.

  6. El análisis de dependencia asigna una estructura sintáctica a una oración y comprende la relación entre las palabras de una oración al enviar las palabras a un analizador de dependencia .
  7. Frases nominales Agrupar frases nominales
    juntas en una oración puede ayudar a simplificar oraciones en casos en los que no nos importan los adjetivos.
  8. Reconocimiento de entidades con nombre Los modelos de reconocimiento de entidades con nombre pueden etiquetar objetos como
    nombres de personas, nombres de empresas y ubicaciones geográficas.
  9. Resolución de correferencia
    Como los modelos de PNL analizan oraciones individuales, pueden confundirse con pronombres que se refieren a sustantivos en otras oraciones. Para abordar esto, empleamos la resolución de correferencia para realizar un seguimiento de los pronombres en oraciones para evitar confusiones.

        Para una descripción más detallada de la PNL: lea esto

        Después de completar estos pasos, nuestro texto está listo para su análisis. Ahora que entendemos mejor la PNL, echemos un vistazo al código de mi proyecto (del Proyecto 5 del curso de comercio de IA de Udacity ). Haga clic aquí para ver el repositorio completo de Github

3. Importación/descarga de datos de PNL

        Primero, hacemos las importaciones necesarias; project_helper contiene varias funciones gráficas y de utilidad.

import nltk
import numpy as np
import pandas as pd
import pickle
import pprint
import project_helper


from tqdm import tqdm

Luego descargamos el corpus de palabras vacías para la eliminación de palabras vacías y el corpus de wordnet para la lematización.

nltk.download('stopwords')
nltk.download('wordnet')

4. Obtenga datos de 10 ks

        La presentación 10-K incluye información como la historia de la empresa, la estructura organizativa, la compensación ejecutiva, el patrimonio, las subsidiarias y los estados financieros auditados. Para buscar documentos 10-k, utilizamos la CIK (clave de índice central) exclusiva de cada empresa.

cik_lookup = {
    'AMZN': '0001018724',
    'BMY': '0000014272',   
    'CNP': '0001130310',
    'CVX': '0000093410',
    'FL': '0000850209',
    'FRT': '0000034903',
    'HON': '0000773840'}

        Ahora tomamos los listados 10-k archivados de la SEC y los mostramos usando datos de Amazon como ejemplo.

sec_api = project_helper.SecAPI()
from bs4 import BeautifulSoup
def get_sec_data(cik, doc_type, start=0, count=60):
    rss_url = 'https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany' \
        '&CIK={}&type={}&start={}&count={}&owner=exclude&output=atom' \
        .format(cik, doc_type, start, count)
    sec_data = sec_api.get(rss_url)
    feed = BeautifulSoup(sec_data.encode('ascii'), 'xml').feed
    entries = [
        (
            entry.content.find('filing-href').getText(),
            entry.content.find('filing-type').getText(),
            entry.content.find('filing-date').getText())
        for entry in feed.find_all('entry', recursive=False)]
return entries
example_ticker = 'AMZN'
sec_data = {}
for ticker, cik in cik_lookup.items():
    sec_data[ticker] = get_sec_data(cik, '10-K')
pprint.pprint(sec_data[example_ticker][:5])

        Recibimos una lista de URL que apuntan a archivos que contienen metadatos asociados con cada relleno. Los metadatos son irrelevantes para nosotros, por lo que extraemos el relleno reemplazando la URL con la URL del relleno. Usemos tqdm para ver el progreso de la descarga y veamos la documentación de muestra.

raw_fillings_by_ticker = {}
for ticker, data in sec_data.items():
    raw_fillings_by_ticker[ticker] = {}
    for index_url, file_type, file_date in tqdm(data, desc='Downloading {} Fillings'.format(ticker), unit='filling'):
        if (file_type == '10-K'):
            file_url = index_url.replace('-index.htm', '.txt').replace('.txtl', '.txt')            
            
            raw_fillings_by_ticker[ticker][file_date] = sec_api.get(file_url)
print('Example Document:\n\n{}...'.format(next(iter(raw_fillings_by_ticker[example_ticker].values()))[:1000]))

        Desglosa el archivo descargado en sus documentos asociados, que están separados en relleno, con etiquetas <DOCUMENT> que indican el comienzo de cada documento y </DOCUMENT> que indican el final de cada documento.

import re
def get_documents(text):
    extracted_docs = []
    
    doc_start_pattern = re.compile(r'<DOCUMENT>')
    doc_end_pattern = re.compile(r'</DOCUMENT>')   
    
    doc_start_is = [x.end() for x in      doc_start_pattern.finditer(text)]
    doc_end_is = [x.start() for x in doc_end_pattern.finditer(text)]
    
    for doc_start_i, doc_end_i in zip(doc_start_is, doc_end_is):
            extracted_docs.append(text[doc_start_i:doc_end_i])
    
    return extracted_docs
filling_documents_by_ticker = {}
for ticker, raw_fillings in raw_fillings_by_ticker.items():
    filling_documents_by_ticker[ticker] = {}
    for file_date, filling in tqdm(raw_fillings.items(), desc='Getting Documents from {} Fillings'.format(ticker), unit='filling'):
        filling_documents_by_ticker[ticker][file_date] = get_documents(filling)
print('\n\n'.join([
    'Document {} Filed on {}:\n{}...'.format(doc_i, file_date, doc[:200])
    for file_date, docs in filling_documents_by_ticker[example_ticker].items()
    for doc_i, doc in enumerate(docs)][:3]))

        Defina la función get_document_type para devolver un tipo de documento determinado.

def get_document_type(doc):
    
    type_pattern = re.compile(r'<TYPE>[^\n]+')
    
    doc_type = type_pattern.findall(doc)[0][len('<TYPE>'):] 
    
    return doc_type.lower()

        Utilice la función get_document_type para filtrar documentos que no sean 10-k del relleno.

ten_ks_by_ticker = {}
for ticker, filling_documents in filling_documents_by_ticker.items():
    ten_ks_by_ticker[ticker] = []
    for file_date, documents in filling_documents.items():
        for document in documents:
            if get_document_type(document) == '10-k':
                ten_ks_by_ticker[ticker].append({
                    'cik': cik_lookup[ticker],
                    'file': document,
                    'file_date': file_date})
project_helper.print_ten_k_data(ten_ks_by_ticker[example_ticker][:5], ['cik', 'file', 'file_date'])

5. Preprocesamiento de datos

        Elimine el html y ponga todo el texto en minúsculas para limpiar el texto del documento.

def remove_html_tags(text):
    text = BeautifulSoup(text, 'html.parser').get_text()
    
    return text
def clean_text(text):
    text = text.lower()
    text = remove_html_tags(text)
    
    return text

        Utilice la función clean_text para limpiar el documento.

for ticker, ten_ks in ten_ks_by_ticker.items():
    for ten_k in tqdm(ten_ks, desc='Cleaning {} 10-Ks'.format(ticker), unit='10-K'):
        ten_k['file_clean'] = clean_text(ten_k['file'])
project_helper.print_ten_k_data(ten_ks_by_ticker[example_ticker][:5], ['file_clean'])

        Ahora lematizamos todos los datos.

from nltk.stem import WordNetLemmatizer
from nltk.corpus import wordnet
def lemmatize_words(words):

    lemmatized_words = [WordNetLemmatizer().lemmatize(word, 'v') for word in words]
    
    return lemmatized_words
word_pattern = re.compile('\w+')
for ticker, ten_ks in ten_ks_by_ticker.items():
    for ten_k in tqdm(ten_ks, desc='Lemmatize {} 10-Ks'.format(ticker), unit='10-K'):
        ten_k['file_lemma'] = lemmatize_words(word_pattern.findall(ten_k['file_clean']))
project_helper.print_ten_k_data(ten_ks_by_ticker[example_ticker][:5], ['file_lemma'])

Elimina las palabras vacías.

from nltk.corpus import stopwords
lemma_english_stopwords = lemmatize_words(stopwords.words('english'))
for ticker, ten_ks in ten_ks_by_ticker.items():
    for ten_k in tqdm(ten_ks, desc='Remove Stop Words for {} 10-Ks'.format(ticker), unit='10-K'):
        ten_k['file_lemma'] = [word for word in ten_k['file_lemma'] if word not in lemma_english_stopwords]
print('Stop Words Removed')

Seis, análisis de sentimientos de 10 ks

        El análisis de opinión se realizó en 10-ks utilizando la lista de palabras de opinión de Loughran-McDonald (que se creó específicamente para el análisis de texto relacionado con las finanzas).

sentiments = ['negative', 'positive', 'uncertainty', 'litigious', 'constraining', 'interesting']

sentiment_df = pd.read_csv('loughran_mcdonald_master_dic_2018.csv')
sentiment_df.columns = [column.lower() for column in sentiment_df.columns] # Lowercase the columns for ease of use

# Remove unused information
sentiment_df = sentiment_df[sentiments + ['word']]
sentiment_df[sentiments] = sentiment_df[sentiments].astype(bool)
sentiment_df = sentiment_df[(sentiment_df[sentiments]).any(1)]

# Apply the same preprocessing to these words as the 10-k words
sentiment_df['word'] = lemmatize_words(sentiment_df['word'].str.lower())
sentiment_df = sentiment_df.drop_duplicates('word')


sentiment_df.head()

Genere bolsas de palabras de opiniones a partir de documentos de 10 k usando listas de palabras de opiniones. Bolsa de palabras cuenta el número de palabras de sentimiento en cada documento.

from collections import defaultdict, Counter
from sklearn.feature_extraction.text import CountVectorizer
def get_bag_of_words(sentiment_words, docs):

    vec = CountVectorizer(vocabulary=sentiment_words)
    vectors = vec.fit_transform(docs)
    words_list = vec.get_feature_names()
    bag_of_words = np.zeros([len(docs), len(words_list)])
    
    for i in range(len(docs)):
        bag_of_words[i] = vectors[i].toarray()[0]
return bag_of_words.astype(int)
sentiment_bow_ten_ks = {}
for ticker, ten_ks in ten_ks_by_ticker.items():
    lemma_docs = [' '.join(ten_k['file_lemma']) for ten_k in ten_ks]
    
    sentiment_bow_ten_ks[ticker] = {
        sentiment: get_bag_of_words(sentiment_df[sentiment_df[sentiment]]['word'], lemma_docs)
        for sentiment in sentiments}
project_helper.print_ten_k_data([sentiment_bow_ten_ks[example_ticker]], sentiments)

7. Similitud Jaccard

        Ahora que tenemos la bolsa de palabras, podemos convertirla en una matriz booleana y calcular la similitud de jaccard. La similitud de Jaccard se define como el tamaño de la intersección dividido por el tamaño de la unión de los dos conjuntos. Por ejemplo, la similitud jaccard entre dos oraciones es la suma de la cantidad de palabras comunes entre las dos oraciones dividida por la cantidad total de palabras únicas en las dos oraciones. Cuanto más cercano a 1 sea el valor de similitud de Jaccard, más similares serán los conjuntos. Para que nuestros cálculos sean más fáciles de entender, trazamos las similitudes de Jaccard.

from sklearn.metrics import jaccard_similarity_score
def get_jaccard_similarity(bag_of_words_matrix):
    
    jaccard_similarities = []
    bag_of_words_matrix = np.array(bag_of_words_matrix, dtype=bool)
    
    for i in range(len(bag_of_words_matrix)-1):
            u = bag_of_words_matrix[i]
            v = bag_of_words_matrix[i+1]
              
    jaccard_similarities.append(jaccard_similarity_score(u,v))    
    
    return jaccard_similarities
# Get dates for the universe
file_dates = {
    ticker: [ten_k['file_date'] for ten_k in ten_ks]
    for ticker, ten_ks in ten_ks_by_ticker.items()}
jaccard_similarities = {
    ticker: {
        sentiment_name: get_jaccard_similarity(sentiment_values)
        for sentiment_name, sentiment_values in ten_k_sentiments.items()}
    for ticker, ten_k_sentiments in sentiment_bow_ten_ks.items()}
project_helper.plot_similarities(
    [jaccard_similarities[example_ticker][sentiment] for sentiment in sentiments],
    file_dates[example_ticker][1:],
    'Jaccard Similarities for {} Sentiment'.format(example_ticker),
    sentiments)

8. TFIDF

        A partir de la lista de palabras de sentimiento, generemos frecuencias de términos de sentimiento: frecuencia de documento inversa (TFIDF) a partir de documentos de 10 k. TFIDF es una técnica de recuperación de información que revela con qué frecuencia aparece una palabra/término en una colección de texto seleccionada. A cada término se le asigna una frecuencia de término (TF) y una puntuación de frecuencia de documento inversa (IDF). El producto de estas puntuaciones se denomina peso TFIDF del término. Los pesos TFIDF más altos indican términos más raros, y los puntajes TFIDF más bajos indican términos más comunes.

from sklearn.feature_extraction.text import TfidfVectorizer
def get_tfidf(sentiment_words, docs):
    
    vec = TfidfVectorizer(vocabulary=sentiment_words)
    tfidf = vec.fit_transform(docs)
    
    return tfidf.toarray()
sentiment_tfidf_ten_ks = {}
for ticker, ten_ks in ten_ks_by_ticker.items():
    lemma_docs = [' '.join(ten_k['file_lemma']) for ten_k in ten_ks]
    
    sentiment_tfidf_ten_ks[ticker] = {
        sentiment: get_tfidf(sentiment_df[sentiment_df[sentiment]]['word'], lemma_docs)
        for sentiment in sentiments}
project_helper.print_ten_k_data([sentiment_tfidf_ten_ks[example_ticker]], sentiments)

9. Semejanza de coseno

        A partir de nuestros valores TFIDF, podemos calcular la similitud del coseno y trazarla como una función del tiempo. Similar a la similitud jaccard, la similitud del coseno es una métrica utilizada para determinar qué tan similares son los documentos. La similitud del coseno calcula la similitud midiendo el coseno del ángulo entre dos vectores proyectados en un espacio multidimensional, independientemente de la magnitud. Para el análisis de texto, los dos vectores utilizados suelen ser matrices que contienen los recuentos de palabras de los dos documentos.

from sklearn.metrics.pairwise import cosine_similarity
def get_cosine_similarity(tfidf_matrix):
    
    cosine_similarities = []    
    
    for i in range(len(tfidf_matrix)-1):
        
cosine_similarities.append(cosine_similarity(tfidf_matrix[i].reshape(1, -1),tfidf_matrix[i+1].reshape(1, -1))[0,0])
    
    return cosine_similarities
cosine_similarities = {
    ticker: {
        sentiment_name: get_cosine_similarity(sentiment_values)
        for sentiment_name, sentiment_values in ten_k_sentiments.items()}
    for ticker, ten_k_sentiments in sentiment_tfidf_ten_ks.items()}
project_helper.plot_similarities(
    [cosine_similarities[example_ticker][sentiment] for sentiment in sentiments],
    file_dates[example_ticker][1:],
    'Cosine Similarities for {} Sentiment'.format(example_ticker),
    sentiments)

10. Datos de precios

        Ahora, evaluaremos el factor alfa comparándolo con el precio anual de las acciones. Podemos descargar datos de precios de QuoteMedia.

pricing = pd.read_csv('yr-quotemedia.csv', parse_dates=['date'])
pricing = pricing.pivot(index='date', columns='ticker', values='adj_close')

pricing

11. Convierta los datos en un marco de datos

        Alphalens es una biblioteca de python para el análisis de rendimiento del factor alfa, utiliza un marco de datos, por lo que tenemos que convertir el diccionario en un marco de datos.


cosine_similarities_df_dict = {'date': [], 'ticker': [], 'sentiment': [], 'value': []}
for ticker, ten_k_sentiments in cosine_similarities.items():
    for sentiment_name, sentiment_values in ten_k_sentiments.items():
        for sentiment_values, sentiment_value in enumerate(sentiment_values):
            cosine_similarities_df_dict['ticker'].append(ticker)
            cosine_similarities_df_dict['sentiment'].append(sentiment_name)
            cosine_similarities_df_dict['value'].append(sentiment_value)
            cosine_similarities_df_dict['date'].append(file_dates[ticker][1:][sentiment_values])
cosine_similarities_df = pd.DataFrame(cosine_similarities_df_dict)
cosine_similarities_df['date'] = pd.DatetimeIndex(cosine_similarities_df['date']).year
cosine_similarities_df['date'] = pd.to_datetime(cosine_similarities_df['date'], format='%Y')
cosine_similarities_df.head()

Antes de aprovechar muchas de las funciones de alphalens, debemos alinear los índices y convertir los tiempos en marcas de tiempo de Unix.

import alphalens as al
factor_data = {}
skipped_sentiments = []
for sentiment in sentiments:
    cs_df = cosine_similarities_df[(cosine_similarities_df['sentiment'] == sentiment)]
    cs_df = cs_df.pivot(index='date', columns='ticker', values='value')
    
    try:
        data = al.utils.get_clean_factor_and_forward_returns(cs_df.stack(), pricing.loc[cs_df.index], quantiles=5, bins=None, periods=[1])
        factor_data[sentiment] = data
    except:
        skipped_sentiments.append(sentiment)
if skipped_sentiments:
    print('\nSkipped the following sentiments:\n{}'.format('\n'.join(skipped_sentiments)))
factor_data[sentiments[0]].head()

        También tuvimos que crear marcos de datos de factores con tiempo de Unix para que fueran compatibles con las funciones factor_rank_autocorrelation y mean_return_by_quantile de alphalen.

unixt_factor_data = {
    factor: data.set_index(pd.MultiIndex.from_tuples(
        [(x.timestamp(), y) for x, y in data.index.values],
        names=['date', 'asset']))
    for factor, data in factor_data.items()}

12. Rentabilidad de los factores

        Veamos los rendimientos de los factores a lo largo del tiempo.

ls_factor_returns = pd.DataFrame()
for factor_name, data in factor_data.items():
    ls_factor_returns[factor_name] = al.performance.factor_returns(data).iloc[:, 0]
(1 + ls_factor_returns).cumprod().plot()

        Como era de esperar, los informes de 10-k que expresaban un sentimiento positivo generaron las mayores ganancias, mientras que los informes de 10-k que contenían un sentimiento negativo generaron las mayores pérdidas.

13. Análisis de la facturación

        Usando la autocorrelación de rango de factores, podemos analizar la estabilidad de alfa a lo largo del tiempo. Queremos que el nivel alfa se mantenga relativamente igual a lo largo del tiempo.

ls_FRA = pd.DataFrame()
for factor, data in unixt_factor_data.items():
    ls_FRA[factor] = al.performance.factor_rank_autocorrelation(data)
ls_FRA.plot(title="Factor Rank Autocorrelation")

14. Relación de Sharpe

        Finalmente, calculemos el índice de Sharpe, que es el rendimiento promedio menos el rendimiento libre de riesgo dividido por la desviación estándar de los rendimientos de la inversión.

daily_annualization_factor = np.sqrt(252)  
(daily_annualization_factor * ls_factor_returns.mean() / ls_factor_returns.std()).round(2)

        Una relación de Sharpe de 1 se considera aceptable, una relación de 2 es muy buena y una relación de 3 es muy buena. Como era de esperar, podemos ver que el sentimiento positivo está asociado con un índice de Sharpe alto y el sentimiento negativo está asociado con un índice de Sharpe bajo. Otras emociones también están asociadas con altos índices de Sharpe. Sin embargo, replicar estos rendimientos en el mundo real es mucho más difícil porque muchos factores complejos afectan los precios de las acciones.

Referencias y citas

[1] Udacity,  inteligencia artificial para el comercio , Github

 

Supongo que te gusta

Origin blog.csdn.net/gongdiwudu/article/details/131865231
Recomendado
Clasificación