Share This
Связаться со мной
Крути в низ
Categories
//Искусственный интеллект для фондового рынка

Искусственный интеллект для фондового рынка

Изучаем на практических примерах, как методы машинного обучения помогают анализировать финансовые отчёты и покупать акции. Обсудить

iskusstvennyj intellekt dlja fondovogo rynka 9c8f03b - Искусственный интеллект для фондового рынка

Применяемые в торговле для прогнозирования модели машинного обучения используют не только исторические цены на акции. Значимую информацию можно извлечь из отчётов по форме 10-К – годовых сводок финансовых результатов, подаваемых компаниями США в комиссию по ценным бумагам и биржам. Анализ отчёта и рыночного отклика на его публикацию помогают инвесторам сделать выводы о необходимости вложения средств в развитие компании. Ускорить процесс обработки данных позволяют технологии обработки естественного языка.

Обработка естественного языка

iskusstvennyj intellekt dlja fondovogo rynka b67034d - Искусственный интеллект для фондового рынка

Схематическое представление конвейера обработки естественного языка

Обработка естественного языка (Natural Language Processing, NLP) – раздел когнитивных технологий, занимающийся обучением компьютеров чтению и извлечению смысла из различных форм речи. Чтобы понять текст, программа должна пройти ряд этапов.

  1. Сегментация предложений. Текстовый документ разбивается на отдельные предложения.
  2. Токенизация. Предложения разделяются на отдельные слова, которые называются токенами.
  3. Частеречная разметка. Каждый токен и несколько слов вокруг него вводятся в предварительно обученную модель классификации, чтобы получить на выходе часть речи.
  4. Лемматизация. Слова часто появляются в разных формах и контекстах. Чтобы компьютер мог выявлять разные формы одного слова, выполняется лемматизация – процесс группировки различных словоизменений для анализа их как единого элемента (идентифицируемого леммой слова).
  5. Стоп-слова. Распространенные части речи, такие как предлоги «и», «в», «на», не несут смысловой нагрузки, поэтому они идентифицируются как стоп-слова и исключаются из анализа текста.
  6. Анализ зависимостей. Назначение синтаксической структуры предложениям и выяснение, как слова в предложении соотносятся друг с другом.
  7. Субстантивные словосочетания. Группировка словосочетаний для упрощения предложений в тех случаях, когда мы не заботимся о прилагательных.
  8. Распознавание именованных сущностей. Модель распознавания именованных сущностей может помечать такие объекты, как имена людей, названия компаний и топонимы.
  9. Разрешение кореферентности. Поскольку модели NLP анализируют отдельные предложения, они «теряются» в местоимениях, относящихся к существительным из других предложений. Чтобы решить эту проблему, используется отслеживающее местоимения в разных предложениях разрешение кореферентности.

Импортируем всё необходимое

Для реализации проекта нам нужно будет использовать Pandas, Numpy и пакет для обработки естественного языка nltk. Попутно будут встречаться и другие библиотеки. Все они легко подключаются через инструмент pip.

         # сторонние библиотеки import nltk import numpy as np import pandas as pd from tqdm import tqdm  # внутренние библиотеки import pickle import pprint  # модуль, описанный ниже import project_helper     

Также нам пригодятся специально написанные модули project_helper (нужно будет установить библиотеку ratelimit) и project_tests. Модуль project_helper содержит служебные и графические функции, project_tests – модульные тесты.

project_helper.py

         import matplotlib.pyplot as plt import requests  from ratelimit import limits, sleep_and_retry  # сторонняя библиотека  class SecAPI(object):     SEC_CALL_LIMIT = {'calls': 10, 'seconds': 1}      @staticmethod     @sleep_and_retry     # Dividing the call limit by half to avoid coming close to the limit     @limits(calls=SEC_CALL_LIMIT['calls'] / 2, period=SEC_CALL_LIMIT['seconds'])     def _call_sec(url):         return requests.get(url)      def get(self, url):         return self._call_sec(url).text   def print_ten_k_data(ten_k_data, fields, field_length_limit=50):     indentation = '  '      print('[')     for ten_k in ten_k_data:         print_statement = '{}{{'.format(indentation)         for field in fields:             value = str(ten_k[field])              # Show return lines in output             if isinstance(value, str):                 value_str = ''{}''.format(value.replace('n', '\n'))             else:                 value_str = str(value)              # Cut off the string if it gets too long             if len(value_str) > field_length_limit:                 value_str = value_str[:field_length_limit] + '...'              print_statement += 'n{}{}: {}'.format(indentation * 2, field, value_str)          print_statement += '},'         print(print_statement)     print(']')   def plot_similarities(similarities_list, dates, title, labels):     assert len(similarities_list) == len(labels)      plt.figure(1, figsize=(10, 7))     for similarities, label in zip(similarities_list, labels):         plt.title(title)         plt.plot(dates, similarities, label=label)         plt.legend()         plt.xticks(rotation=90)      plt.show()     

project_tests.py

         import numpy as np import pandas as pd  from collections import OrderedDict  from tests import assert_output, project_test, assert_structure   @project_test def test_get_documents(fn):     # Test 1     doc = 'nThis is inside the documentn'            'This is the text that should be copied'     text = 'This is before the test document<DOCUMENT>{}</DOCUMENT>n'             'This is after the documentn'             'This shouldnt be included.'.format(doc)      fn_inputs = {         'text': text}     fn_correct_outputs = OrderedDict([         (             'extracted_docs', [doc])])      assert_output(fn, fn_inputs, fn_correct_outputs, check_parameter_changes=False)      # Test 2     ten_k_real_compressed_doc = 'n'          '<TYPE>10-Kn'          '<SEQUENCE>1n'          '<FILENAME>test-20171231x10k.htmn'          '<DESCRIPTION>10-Kn'          '<TEXT>n'          '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">n'          '<html>n'          '	<head>n'          '		<title>Document</title>n'          '	</head>n'          '	<body style="font-family:Times New Roman;font-size:10pt;">n'          '...n'          '<td><strong> Data Type:</strong></td>n'          '<td>xbrli:sharesItemType</td>n'          '</tr>n'          '<tr>n'          '<td><strong> Balance Type:</strong></td>n'          '<td>na</td>n'          '</tr>n'          '<tr>n'          '<td><strong> Period Type:</strong></td>n'          '<td>duration</td>n'          '</tr>n'          '</table></div>n'          '</div></td></tr>n'          '</table>n'          '</div>n'          '</body>n'          '</html>n'          '</TEXT>n'     excel_real_compressed_doc = 'n'          '<TYPE>EXCELn'          '<SEQUENCE>106n'          '<FILENAME>Financial_Report.xlsxn'          '<DESCRIPTION>IDEA: XBRL DOCUMENTn'          '<TEXT>n'          'begin 644 Financial_Report.xlsxn'          'M4$L#!!0    ( %"E04P?(\#P    !,"   +    7W)E;,O+G)E;.MDD^+n'          'MPD ,Q;]*F?L:5#8CUYZ6U9_ )Q)OU#.Y,A$[%^>X>];+=44/ 87O+>CT?Vn'          '...n'          'M,C,Q7V1E9BYX;6Q02P$"% ,4    " !0I4%,>V7[]F0L 0!(@A  %0n'          'M        @ %N9@, 86UZ;BTR,#$W,3(S,5]L86(N>&UL4$L! A0#%     @n'          'M4*5!3*U*Q:W#O0  U=) !4              ( !!9,$ &%M>FXM,C Q-S$Rn'          '@,S%?<)E+GAM;%!+!08     !@ & (H!  #[4 4    !n'          'n'          'endn'          '</TEXT>n'     real_compressed_text = '<SEC-DOCUMENT>0002014754-18-050402.txt : 20180202n'          '<SEC-HEADER>00002014754-18-050402.hdr.sgml : 20180202n'          '<ACCEPTANCE-DATETIME>20180201204115n'          'ACCESSION NUMBER:		0002014754-18-050402n'          'CONFORMED SUBMISSION TYPE:	10-Kn'          'PUBLIC DOCUMENT COUNT:		110n'          'CONFORMED PERIOD OF REPORT:	20171231n'          'FILED AS OF DATE:		20180202n'          'DATE AS OF CHANGE:		20180201n'          'n'          'FILER:n'          'n'          '	COMPANY DATA:	n'          '		COMPANY CONFORMED NAME:			TESTn'          '		CENTRAL INDEX KEY:			0001018724n'          '		STANDARD INDUSTRIAL CLASSIFICATION:	RANDOM [2357234]n'          '		IRS NUMBER:				91236464620n'          '		STATE OF INCORPORATION:			DEn'          '		FISCAL YEAR END:			1231n'          'n'          '	FILING VALUES:n'          '		FORM TYPE:		10-Kn'          '		SEC ACT:		1934 Actn'          '		SEC FILE NUMBER:	000-2225413n'          '		FILM NUMBER:		13822526583969n'          'n'          '	BUSINESS ADDRESS:	n'          '		STREET 1:		422320 PLACE AVENUEn'          '		CITY:			SEATTLEn'          '		STATE:			WAn'          '		ZIP:			234234n'          '		BUSINESS PHONE:		306234534246600n'          'n'          '	MAIL ADDRESS:	n'          '		STREET 1:		422320 PLACE AVENUEn'          '		CITY:			SEATTLEn'          '		STATE:			WAn'          '		ZIP:			234234n'          '</SEC-HEADER>n'          '<DOCUMENT>{}</DOCUMENT>n'          '<DOCUMENT>{}</DOCUMENT>n'          '</SEC-DOCUMENT>n'.format(ten_k_real_compressed_doc, excel_real_compressed_doc)      fn_inputs = {         'text': real_compressed_text}     fn_correct_outputs = OrderedDict([         (             'extracted_docs', [ten_k_real_compressed_doc, excel_real_compressed_doc])])      assert_output(fn, fn_inputs, fn_correct_outputs, check_parameter_changes=False)   @project_test def test_get_document_type(fn):     doc = 'n'          '<TYPE>10-Kn'          '<SEQUENCE>1n'          '<FILENAME>test-20171231x10k.htmn'          '<DESCRIPTION>10-Kn'          '<TEXT>n'          '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">n'          '...'      fn_inputs = {         'doc': doc}     fn_correct_outputs = OrderedDict([         (             'doc_type', '10-k')])      assert_output(fn, fn_inputs, fn_correct_outputs, check_parameter_changes=False)   @project_test def test_lemmatize_words(fn):     fn_inputs = {         'words': ['cow', 'running', 'jeep', 'swimmers', 'tackle', 'throw', 'driven']}     fn_correct_outputs = OrderedDict([         (             'lemmatized_words', ['cow', 'run', 'jeep', 'swimmers', 'tackle', 'throw', 'drive'])])      assert_output(fn, fn_inputs, fn_correct_outputs, check_parameter_changes=False)   @project_test def test_get_bag_of_words(fn):     def sort_ndarray(array):         hashes = [hash(str(x)) for x in array]         sotred_indicies = sorted(range(len(hashes)), key=lambda k: hashes[k])          return array[sotred_indicies]      fn_inputs = {         'sentiment_words': pd.Series(['one', 'last', 'second']),         'docs': [             'this is a document',             'this document is the second document',             'last one']}     fn_correct_outputs = OrderedDict([         (             'bag_of_words', np.array([                 [0, 0, 0],                 [1, 0, 0],                 [0, 1, 1]]))])      fn_out = fn(**fn_inputs)     assert_structure(fn_out, fn_correct_outputs['bag_of_words'], 'bag_of_words')     assert np.array_equal(sort_ndarray(fn_out.T), sort_ndarray(fn_correct_outputs['bag_of_words'].T)),          'Wrong value for bag_of_words.n'          'INPUT docs:n{}nn'          'OUTPUT bag_of_words:n{}nn'          'A POSSIBLE CORRECT OUTPUT FOR bag_of_words:n{}n'         .format(fn_inputs['docs'], fn_out, fn_correct_outputs['bag_of_words'])   @project_test def test_get_jaccard_similarity(fn):     fn_inputs = {         'bag_of_words_matrix': np.array([                 [0, 1, 1, 0, 0, 0, 1],                 [0, 1, 2, 0, 1, 1, 1],                 [1, 0, 0, 1, 0, 0, 0]])}     fn_correct_outputs = OrderedDict([         (             'jaccard_similarities', [0.7142857142857143, 0.0])])      assert_output(fn, fn_inputs, fn_correct_outputs, check_parameter_changes=False)   @project_test def test_get_tfidf(fn):     def sort_ndarray(array):         hashes = [hash(str(x)) for x in array]         sotred_indicies = sorted(range(len(hashes)), key=lambda k: hashes[k])          return array[sotred_indicies]      fn_inputs = {         'sentiment_words': pd.Series(['one', 'last', 'second']),         'docs': [             'this is a document',             'this document is the second document',             'last one']}     fn_correct_outputs = OrderedDict([         (             'tfidf', np.array([                 [0.0, 0.0, 0.0],                 [1.0, 0.0, 0.0],                 [0.0, 0.70710678, 0.70710678]]))])      fn_out = fn(**fn_inputs)     assert_structure(fn_out, fn_correct_outputs['tfidf'], 'tfidf')     assert np.isclose(sort_ndarray(fn_out.T), sort_ndarray(fn_correct_outputs['tfidf'].T)).all(),          'Wrong value for tfidf.n'          'INPUT docs:n{}nn'          'OUTPUT tfidf:n{}nn'          'A POSSIBLE CORRECT OUTPUT FOR tfidf:n{}n'         .format(fn_inputs['docs'], fn_out, fn_correct_outputs['tfidf'])   @project_test def test_get_cosine_similarity(fn):     fn_inputs = {         'tfidf_matrix': np.array([                 [0.0,           0.57735027, 0.57735027, 0.0,        0.0,        0.0,        0.57735027],                 [0.0,           0.32516555, 0.6503311,  0.0,        0.42755362, 0.42755362, 0.32516555],                 [0.70710678,    0.0,        0.0,        0.70710678, 0.0,        0.0,        0.0]])}     fn_correct_outputs = OrderedDict([         (             'cosine_similarities', [0.75093766927060945, 0.0])])      assert_output(fn, fn_inputs, fn_correct_outputs, check_parameter_changes=False)     

Затем мы загружаем стоп-слова для их удаления и wordnet для лемматизации.

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

Всю дальнейшую обработку будем производить в Jupyter Notebook (GitHub-репозиторий).

Получение 10-К

Отчёты 10-K включают описание истории компании, организационной структуры, вознаграждений руководителей, собственного капитала, капитала дочерних компаний и аудиты финансовой отчётности. Для поиска документов можно использовать уникальный для каждой компании CIK (Central Index Key). Перечислим их примеры в виде словаря:

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

Выведем для примера данные Amazon. Подключение к сайту, где хранятся отчёты, производится с помощью упомянутого выше модуля project_helper:

         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])     

В результате получаем следующий вывод:

Результат выполнения программного блока

         [('https://www.sec.gov/Archives/edgar/data/1018724/000101872420000004/0001018724-20-000004-index.htm',   '10-K',   '2020-01-31'),  ('https://www.sec.gov/Archives/edgar/data/1018724/000101872419000004/0001018724-19-000004-index.htm',   '10-K',   '2019-02-01'),  ('https://www.sec.gov/Archives/edgar/data/1018724/000101872418000005/0001018724-18-000005-index.htm',   '10-K',   '2018-02-02'),  ('https://www.sec.gov/Archives/edgar/data/1018724/000101872417000011/0001018724-17-000011-index.htm',   '10-K',   '2017-02-10'),  ('https://www.sec.gov/Archives/edgar/data/1018724/000101872416000172/0001018724-16-000172-index.htm',   '10-K',   '2016-01-29')]     

Теперь у нас есть список URL-адресов, указывающих на файлы с метаданными, остаётся только забрать информацию:

         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:nn{}...'.format(next(iter(raw_fillings_by_ticker[example_ticker].values()))[:1000]))     

Процесс загрузки данных, содержащих отчеты (отображение прогресс-бара осуществляется с помощью tqdm)

         Downloading AMZN Fillings: 100%|██████████| 25/25 [00:05<00:00,  4.88filling/s] Downloading BMY Fillings: 100%|██████████| 30/30 [00:12<00:00,  2.49filling/s] Downloading CNP Fillings: 100%|██████████| 22/22 [00:57<00:00,  2.61s/filling] Downloading CVX Fillings: 100%|██████████| 28/28 [00:34<00:00,  1.24s/filling] Downloading FL Fillings: 100%|██████████| 25/25 [00:25<00:00,  1.02s/filling] Downloading FRT Fillings: 100%|██████████| 32/32 [00:36<00:00,  1.15s/filling] Downloading HON Fillings:  29%|██▊       | 8/28 [00:10<00:25,  1.30s/filling]     

Первая 1000 символов примера загруженного  отчёта

         Example Document:  <SEC-DOCUMENT>0001018724-20-000004.txt : 20200131 <SEC-HEADER>0001018724-20-000004.hdr.sgml : 20200131 <ACCEPTANCE-DATETIME>20200130204613 ACCESSION NUMBER:		0001018724-20-000004 CONFORMED SUBMISSION TYPE:	10-K PUBLIC DOCUMENT COUNT:		109 CONFORMED PERIOD OF REPORT:	20191231 FILED AS OF DATE:		20200131 DATE AS OF CHANGE:		20200130  FILER:  	COMPANY DATA:	 		COMPANY CONFORMED NAME:			AMAZON COM INC 		CENTRAL INDEX KEY:			0001018724 		STANDARD INDUSTRIAL CLASSIFICATION:	RETAIL-CATALOG & MAIL-ORDER HOUSES [5961] 		IRS NUMBER:				911646860 		STATE OF INCORPORATION:			DE 		FISCAL YEAR END:			1231  	FILING VALUES: 		FORM TYPE:		10-K 		SEC ACT:		1934 Act 		SEC FILE NUMBER:	000-22513 		FILM NUMBER:		20562951  	BUSINESS ADDRESS:	 		STREET 1:		410 TERRY AVENUE NORTH 		CITY:			SEATTLE 		STATE:			WA 		ZIP:			98109 		BUSINESS PHONE:		2062661000  	MAIL ADDRESS:	 		STREET 1:		410 TERRY AVENUE NORTH 		CITY:			SEATTLE 		STATE:			WA 		ZIP:			98109 </SEC-HEADER> <DOCUMENT> <TYPE>10-K <SEQUENCE>1 <FILENAM...     

Разобьем загруженные файлы на документы по ограничивающим тегам <DOCUMENT> и </DOCUMENT>:

         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('nn'.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]))     

Процесс преобразования документов

         Getting Documents from AMZN Fillings: 100%|██████████| 20/20 [00:00<00:00, 71.03filling/s] Getting Documents from BMY Fillings: 100%|██████████| 26/26 [00:00<00:00, 42.15filling/s] Getting Documents from CNP Fillings: 100%|██████████| 18/18 [00:00<00:00, 33.56filling/s] Getting Documents from CVX Fillings: 100%|██████████| 24/24 [00:00<00:00, 36.84filling/s] Getting Documents from FL Fillings: 100%|██████████| 19/19 [00:00<00:00, 58.05filling/s] Getting Documents from FRT Fillings: 100%|██████████| 22/22 [00:00<00:00, 58.46filling/s] Getting Documents from HON Fillings: 100%|██████████| 23/23 [00:00<00:00, 46.45filling/s]     

Результаты разбиения

         Document 0 Filed on 2020-01-31:  <TYPE>10-K <SEQUENCE>1 <FILENAME>amzn-20191231x10k.htm <DESCRIPTION>10-K <TEXT> <XBRL> <?xml version="1.0" encoding="UTF-8"?> <!--XBRL Document Created with Wdesk from Workiva--> <!--p:c57a17684e854b...  Document 1 Filed on 2020-01-31:  <TYPE>EX-4.6 <SEQUENCE>2 <FILENAME>amzn-20191231xex46.htm <DESCRIPTION>EXHIBIT 4.6 <TEXT> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html>...  Document 2 Filed on 2020-01-31:  <TYPE>EX-21.1 <SEQUENCE>3 <FILENAME>amzn-20191231xex211.htm <DESCRIPTION>EXHIBIT 21.1 <TEXT> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <ht...     

Определим функцию get_document_type() для возврата типа документа:

         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()     

Отфильтруем с помощью написанной функции все не являющиеся отчетами документы из вложений:

         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'])     
         [   {     cik: '0001018724'     file: 'n<TYPE>10-Kn<SEQUENCE>1n<FILENAME>amzn-2019123...     file_date: '2020-01-31'},   {     cik: '0001018724'     file: 'n<TYPE>10-Kn<SEQUENCE>1n<FILENAME>amzn-2018123...     file_date: '2019-02-01'},   {     cik: '0001018724'     file: 'n<TYPE>10-Kn<SEQUENCE>1n<FILENAME>amzn-2017123...     file_date: '2018-02-02'},   {     cik: '0001018724'     file: 'n<TYPE>10-Kn<SEQUENCE>1n<FILENAME>amzn-2016123...     file_date: '2017-02-10'},   {     cik: '0001018724'     file: 'n<TYPE>10-Kn<SEQUENCE>1n<FILENAME>amzn-2015123...     file_date: '2016-01-29'}, ]     

Предобработка данных

Удалим html-код и переведем все символы в строчные:

         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     

Очистим документы с помощью функции clean_text():

         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'])     
         Cleaning AMZN 10-Ks: 100%|██████████| 20/20 [00:26<00:00,  1.31s/10-K] Cleaning BMY 10-Ks: 100%|██████████| 26/26 [00:56<00:00,  2.17s/10-K] Cleaning CNP 10-Ks: 100%|██████████| 18/18 [00:54<00:00,  3.02s/10-K] Cleaning CVX 10-Ks: 100%|██████████| 24/24 [01:44<00:00,  4.33s/10-K] Cleaning FL 10-Ks: 100%|██████████| 19/19 [00:21<00:00,  1.14s/10-K] Cleaning FRT 10-Ks: 100%|██████████| 22/22 [00:43<00:00,  1.97s/10-K] Cleaning HON 10-Ks: 100%|██████████| 23/23 [00:46<00:00,  2.02s/10-K] [   {     file_clean: 'n10-kn1namzn-20191231x10k.htmn10-knnnnn...},   {     file_clean: 'n10-kn1namzn-20181231x10k.htmn10-knnnnn...},   {     file_clean: 'n10-kn1namzn-20171231x10k.htmn10-knnnnn...},   {     file_clean: 'n10-kn1namzn-20161231x10k.htmnform 10-knnn...},   {     file_clean: 'n10-kn1namzn-20151231x10k.htmnform 10-knnn...}, ]     

Лемматизируем данные:

         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'])     

Вывод

         Lemmatize AMZN 10-Ks: 100%|██████████| 20/20 [00:03<00:00,  5.4210-K/s] Lemmatize BMY 10-Ks: 100%|██████████| 26/26 [00:04<00:00,  5.3810-K/s] Lemmatize CNP 10-Ks: 100%|██████████| 18/18 [00:04<00:00,  3.9610-K/s] Lemmatize CVX 10-Ks: 100%|██████████| 24/24 [00:04<00:00,  5.0810-K/s] Lemmatize FL 10-Ks: 100%|██████████| 19/19 [00:02<00:00,  9.2110-K/s] Lemmatize FRT 10-Ks: 100%|██████████| 22/22 [00:03<00:00,  7.0410-K/s] Lemmatize HON 10-Ks: 100%|██████████| 23/23 [00:03<00:00,  7.6110-K/s] [   {     file_lemma: '['10', 'k', '1', 'amzn', '20191231x10k', 'htm', '...},   {     file_lemma: '['10', 'k', '1', 'amzn', '20181231x10k', 'htm', '...},   {     file_lemma: '['10', 'k', '1', 'amzn', '20171231x10k', 'htm', '...},   {     file_lemma: '['10', 'k', '1', 'amzn', '20161231x10k', 'htm', '...},   {     file_lemma: '['10', 'k', '1', 'amzn', '20151231x10k', 'htm', '...}, ]     

Удаляем стоп-слова:

         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')     
         Remove Stop Words for AMZN 10-Ks: 100%|██████████| 20/20 [00:01<00:00, 15.8810-K/s] Remove Stop Words for BMY 10-Ks: 100%|██████████| 26/26 [00:02<00:00,  9.8610-K/s] Remove Stop Words for CNP 10-Ks: 100%|██████████| 18/18 [00:02<00:00,  7.5410-K/s] Remove Stop Words for CVX 10-Ks: 100%|██████████| 24/24 [00:02<00:00,  9.4010-K/s] Remove Stop Words for FL 10-Ks: 100%|██████████| 19/19 [00:01<00:00, 17.5510-K/s] Remove Stop Words for FRT 10-Ks: 100%|██████████| 22/22 [00:01<00:00, 13.2810-K/s] Remove Stop Words for HON 10-Ks: 100%|██████████| 23/23 [00:01<00:00, 15.1510-K/s] Stop Words Removed     

Анализ тональностей 10-К

Воспользуемся списком слов Loughran-McDonald для выполнения анализа тональностей текстов в 10-К. Он был специально разработан для текстового анализа с финансовой составляющей.

         sentiments = ['negative', 'positive', 'uncertainty', 'litigious', 'constraining'] sentiment_df = pd.read_csv('loughran_mcdonald_master_dic_2018.csv') sentiment_df.columns = [column.lower() for column in sentiment_df.columns]  # Удалим неиспользующуюся информацию sentiment_df = sentiment_df[sentiments + ['word']] sentiment_df[sentiments] = sentiment_df[sentiments].astype(bool) sentiment_df = sentiment_df[(sentiment_df[sentiments]).any(1)]  # Применим ту же предобработку данных, что для отчетов 10-K sentiment_df['word'] = lemmatize_words(sentiment_df['word'].str.lower()) sentiment_df = sentiment_df.drop_duplicates('word')   sentiment_df.head()     

iskusstvennyj intellekt dlja fondovogo rynka 830f176 - Искусственный интеллект для фондового рынка

Первые пять строк подготовленного датасета

Создадим набор тональностей слов для документов 10-К, используя списки тональностей. Подсчитаем количество слов с определённой тональностью в каждом документе:

         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)     
         [   {     negative: '[[0 0 0 ... 0 0 0]n [0 0 0 ... 0 0 0]n [0 0 0 ....     positive: '[[12  0  0 ...  0  0  0]n [15  0  0 ...  0  0  0...     uncertainty: '[[0 0 0 ... 1 1 2]n [0 0 0 ... 1 1 2]n [0 0 0 ....     litigious: '[[0 0 0 ... 0 0 0]n [0 0 0 ... 0 0 0]n [0 0 0 ....     constraining: '[[0 0 0 ... 0 0 2]n [0 0 0 ... 0 0 2]n [0 0 0 ....}, ]     

Сходство Жаккара

Когда у нас есть набор слов, мы можем преобразовать его в логический массив и вычислить коэффициент сходства Жаккара. Сходство определяется как размер пересечения множеств, поделенный на размер их объединения.

Например, сходство Жаккара между двумя предложениями – это количество общих слов в двух предложениях, поделенное на общее количество уникальных слов в обоих предложениях. Чем ближе значение сходства к 1, тем более похожи наборы. Для наглядности построим график коэффициента сходства Жаккара.

         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)     

iskusstvennyj intellekt dlja fondovogo rynka baac48c - Искусственный интеллект для фондового рынка

TF-IDF

Из списков слов тональности создадим term frequency – inverse document frequency (TF-IDF) для документов 10-К. TF-IDF – это метод поиска информации, используемый для выявления частоты появления слова в выбранной коллекции текста. Каждому слову/термину присваивается частота термина (TF) и обратная частота документа (IDF). Произведение этих оценок называется весом TF-IDF данного термина. Высокий вес TF-IDF указывает на редкие термины, а низкий – на более общие.

         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)     

iskusstvennyj intellekt dlja fondovogo rynka 653db7a - Искусственный интеллект для фондового рынка

Косинусное сходство

Исходя из значений TF-IDF, мы можем вычислить косинусное сходство и построить его с учетом изменения времени. Подобно сходству Жаккара – это метрика, используемая для определения схожести документов. Косинус схожести вычисляет подобие независимо от размера, измеряя косинус угла между двумя векторами, проецируемыми в многомерном пространстве. Для текстового анализа обычно используются массивы, содержащие количество слов в двух документах.

         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)     

iskusstvennyj intellekt dlja fondovogo rynka 8577944 - Искусственный интеллект для фондового рынка

Ценовые данные

Теперь мы оценим альфа-факторы, сравнив их с годовыми ценами на акции. Мы можем скачать данные о ценах из QuoteMedia.

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

Преобразование в датафрейм

Alphalens – использующая датафреймы библиотека Python для анализа производительности альфа-факторов поможет преобразовать наш словарь.

         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()     

iskusstvennyj intellekt dlja fondovogo rynka b3ed711 - Искусственный интеллект для фондового рынка

Перед использованием функции alphalens нужно выровнять индексы и преобразовать время в unix timestamp:

         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()     

iskusstvennyj intellekt dlja fondovogo rynka 85169f9 - Искусственный интеллект для фондового рынка

Мы также должны создать факторные датафреймы с unix-временем для совместимости с alphalen-функциями: factor_rank_autocorrelation и mean_return_by_quantile:

         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()}     

Коэффициент отдачи

Посмотрим на коэффициент отдачи с течением времени:

         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()     

iskusstvennyj intellekt dlja fondovogo rynka 15ba732 - Искусственный интеллект для фондового рынка

Выражающие позитивную тональность отчеты 10-К свидетельствуют о наибольшей прибыли, а негативные – об убытках.

Анализ оборота

Используя factor_rank_autocorrelation, мы можем проанализировать, насколько устойчивы альфы с течением времени. Мы хотим, чтобы они оставались относительно одинаковыми от периода к периоду:

         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")     

iskusstvennyj intellekt dlja fondovogo rynka e0e6da3 - Искусственный интеллект для фондового рынка

Коэффициент Шарпа

Теперь давайте рассчитаем коэффициент Шарпа, который представляет собой разность между средней доходностью и безрисковой доходностью, поделённой на стандартное отклонение доходности инвестиций.

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

iskusstvennyj intellekt dlja fondovogo rynka e8d921c - Искусственный интеллект для фондового рынка

Коэффициент Шарпа 1 считается приемлемым, 2 – очень хорошим, а 3 – превосходным. Положительная тональность коррелирует с высоким коэффициентом Шарпа, а отрицательная – с низким. Другие настроения также коррелировали с высокими коэффициентами.

Заключение

В статье мы рассмотрели занимательную тему обработки естественного языка. Этот подход применяется на фондовых рынках, поскольку дает массу информации для понимания процессов и помогает инвесторам принять решение о вложении средств. Тема непростая, но она будет интересна не только работающим в финансовой сфере специалистам, но и Python-разработчикам, желающим окунуться в мир возвратов и оборотов.

***

  • NLP – это весело! Обработка естественного языка на Python
  • Обработка естественного языка: с чего начать и что изучать дальше

  • 16 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Связаться со мной
Close