CoderCastrov logo
CoderCastrov
Питон

Используйте свой компьютер для принятия обоснованных решений при торговле акциями: Практическое введение — Часть 4: Парсинг прибыли на акцию (EPS)

Используйте свой компьютер для принятия обоснованных решений при торговле акциями: Практическое введение — Часть 4: Парсинг прибыли на акцию (EPS)
просмотров
17 мин чтение
#Питон
The Metal Man of Sligo lighthouse, Ireland

Введение

Я точно помню день, когда начал заниматься торговлей акциями — это было 28 апреля 2020 года, когда Google объявил о своей прибыли за первый квартал и поднялся на более чем 7% за 1 день. Я подумал, что у Facebook похожий бизнес, и он представит отчет на следующий день, поэтому я вложил свои первые $1000 в акции Facebook. И это была быстрая победа — они поднялись на более чем 10% за 1 день после показа "стабильности в доходе от рекламы после падения в марте". Та же стратегия сработала и для акций Facebook во время отчета за второй квартал: они поднялись на 8% после сообщения о более высокой прибыли на акцию $1.8 вместо ожидаемых $1.39. После второго успеха я наконец решил провести полноценный анализ в масштабе и написать статью об этом, которую вы можете прочитать ниже.

Гипотеза состоит в следующем: "Финансовые результаты становятся чрезвычайно важными в трудные времена (например, COVID-19 в 2020 году): некоторые устойчивые отрасли получают диспропорционально высокий объем торговли акциями и рост цен".

Я хотел бы найти хотя бы одну сильную идею, которая будет работать большую часть времени. Просто как тот Металлический человек из Слайго на картинке, указывающий на вход в Слайго уже несколько веков.


Подход

Владение акциями дает вам право на получение части прибыли компании. В идеальном мире цена акций должна зависеть от прибыли компании, так как это дисконтированные будущие прибыли. Если в какой-то момент компания зарабатывает больше, чем раньше, это может означать, что рост компании ускоряется, и цена акций должна быть выше. Поэтому одним из самых важных финансовых показателей является прибыль на акцию (EPS). Каждый квартал аналитики делают прогнозы о прибыли (или убытках) компании, а затем сравнивают эти прогнозы с фактической отчетностью EPS. Если компания делает лучше, чем прогнозировалось, это должно вызвать рост цены акций, и наоборот.

В этой статье мы собираемся проверить это в масштабе — для сотен акций, которые сообщили о прибыли во втором квартале 2020 года. Мы проверим зависимость изменения цены акций от фактической прибыли на акцию, прогнозируемой прибыли на акцию и Surprise (= фактическая_EPS/прогнозируемая_EPS-1). Ниже представлен краткий обзор разделов и тем, рассмотренных в статье:

В разделе Парсинг Yahoo Finance: Прибыль на акцию вы узнаете, как получить информацию о прибыли на акцию для широкого спектра компаний за указанный период времени (начиная с одного дня), парся ее с веб-сайта Yahoo Finance. Затем, в разделе Упаковка всего в одну функцию парсинга, вы объедините все, что вы узнали в предыдущем разделе, в одну функцию, чтобы получить еженедельную статистику по датам и EPS. После этого, в разделе Получение данных о ценах акций для компании, вы рассмотрите, как можно получить данные о доходности акций и объеме для определенной компании. В разделе Получение статистики S&P 500, вы узнаете, как получить данные S&P 500 для оценки того, как определенный символ справляется по сравнению с индексом. В разделе Получение доходности акций и объема с Yahoo Finance вы узнаете, как получить данные о доходности акций и объеме для всех тикеров, найденных в Yahoo Finance. В разделе Слияние всех частей вместе вы получите объединенный фрейм данных со статистикой из всех предыдущих частей. И, наконец, в разделе Анализ и визуализация вы увидите примеры графиков, построенных на основе набора данных.

Эта статья является четвертой частью серии, которая охватывает использование компьютерных технологий для принятия обоснованных решений в торговле акциями. Обратитесь к части 1, чтобы быть проведенным через процесс настройки рабочей среды, необходимой для следования примерам, представленным в остальной части серии. Затем, часть 2 рассматривает несколько известных финансовых API, позволяющих получать и анализировать данные о акциях программно. В части 3 вы исследовали, влияет ли рынок акций на новости.

Парсинг Yahoo Finance: Прибыль на акцию

Вам может быть интересна информация о прибыли на акцию для широкого спектра компаний за определенный период времени. Вы можете получить эту информацию, выполнив парсинг веб-сайта Yahoo Finance по адресу https://finance.yahoo.com/calendar/earnings:

Для парсинга вам понадобится установить библиотеку BeautifulSoup в Colab:

!pip install beautifulsoup4

Затем убедитесь, что импортированы следующие библиотеки:

import requests
from bs4 import BeautifulSoup
import pandas as pd
import numpy as np

Предположим, вы хотите получить данные за 2020–07–27. Вам нужно указать следующий URL:

url = “https://finance.yahoo.com/calendar/earnings?from=2020-07-26&to=2020-08-01&day=2020-07-27"

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

r = requests.get(url)

Чтобы убедиться, что все прошло как ожидалось, проверьте статус запроса:

r.ok

Если все в порядке, вы должны увидеть:

True

Теперь вы можете перейти к контенту:

r.content

Однако ваша задача - найти данные таблицы внутри него. Это можно легко сделать с помощью BeautifulSoup следующим образом:

soup = BeautifulSoup(r.text)
table = soup.find_all(‘table’)
len(table)#output:
1

Найдена только одна таблица, что хорошо. Теперь получим все названия столбцов таблицы:

spans = soup.table.thead.find_all(‘span’)
columns = []
for span in spans:
  print(span.text)
  columns.append(span.text)

Вот столбцы (обратитесь к скриншоту на рисунке 1, чтобы убедиться, что все столбцы были найдены):

Символ
Компания
Время звонка о прибыли
Прогноз прибыли на акцию
Отчетная прибыль на акцию
Сюрприз(%)

Теперь вы можете перейти к строкам:

rows = soup.table.tbody.find_all(‘tr’)
len(rows)

Как видите, у вас есть 100 строк в таблице, полученной с веб-страницы. В следующем фрагменте кода вы загружаете строки в pandas dataframe, читая их построчно:

stocks_df = pd.DataFrame(columns=columns)for row in rows:
  elems = row.find_all(‘td’)
  dict_to_add = {}
  for i,elem in enumerate(elems):
    dict_to_add[columns[i]] = elem.text
  stocks_df = stocks_df.append(dict_to_add, ignore_index=True)

В результате данные в dataframe должны выглядеть следующим образом:

stocks_df

У вас должно быть 100 полученных строк. Мы будем использовать все столбцы, но обратите внимание, что значения времени звонка о прибыли не указаны во многих случаях. Некоторые другие проблемы в наборе данных:

  • отсутствующие значения: некоторые значения для прогноза прибыли на акцию, отчетной прибыли на акцию и сюрприза неизвестны
  • некоторые значения в этих столбцах являются целыми числами: их нужно преобразовать в числа с плавающей запятой

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

filter1 = stocks_df[‘Сюрприз(%)’]!=’-’
filter2 = stocks_df[‘Прогноз прибыли на акцию’]!=’-’
filter3 = stocks_df[‘Отчетная прибыль на акцию’]!=’-’stocks_df_noMissing = stocks_df[filter1 & filter2 & filter3]

Вы должны увидеть, что количество строк уменьшилось после этого:

len(stocks_df_noMissing)79

На следующем шаге вы решаете другую проблему и преобразуете все значения в столбцах "Прогноз прибыли на акцию", "Отчетная прибыль на акцию" и "Сюрприз" в числа с плавающей запятой:

stocks_df_noMissing[‘Прогноз прибыли на акцию’] = stocks_df_noMissing[‘Прогноз прибыли на акцию’].astype(float)stocks_df_noMissing[‘Отчетная прибыль на акцию’] = stocks_df_noMissing[‘Отчетная прибыль на акцию’].astype(float)stocks_df_noMissing[‘Сюрприз(%)’] = stocks_df_noMissingstocks_df_noMissing[‘Сюрприз(%)’].astype(float)

В результате у вас должен быть следующий dataframe:

stocks_df_noMissing.info()<class ‘pandas.core.frame.DataFrame’>
Int64Index: 79 entries, 0 to 99
Data columns (total 6 columns):
# Column Non-Null Count Dtype
 — — — — — — — — — — — — — — -
0 Символ 79 non-null object
1 Компания 79 non-null object
2 Время звонка о прибыли 79 non-null object
3 Прогноз прибыли на акцию 79 non-null float64
4 Отчетная прибыль на акцию 79 non-null float64
5 Сюрприз(%) 79 non-null float64
import requests
from bs4 import BeautifulSoup
import pandas as pd
import numpy as nm
from datetime import datetime, timedelta, date

def get_scrapped_week(from_dt, to_dt):
    url = "https://finance.yahoo.com/calendar/earnings"
    offset = 0
    size = 100
    fst = 1
    
    for day_date in (datetime.strptime(from_dt, '%Y-%m-%d') + timedelta(n) for n in range(6)):
        day_dt = datetime.strftime(day_date, '%Y-%m-%d')
        print(day_dt)
        
        while True:
            params = {'from': from_dt, 'to': to_dt, 'day': day_dt, 'offset':offset, 'size': size}
            r = requests.get(url, params=params)
            soup = BeautifulSoup(r.text)
            
            if fst == 1:
                spans = soup.table.thead.find_all('span')
                columns = []
                for span in spans:
                    print(span.text)
                    columns.append(span.text)
                stocks_df = pd.DataFrame(columns=columns)
                fst = 0
            
            rows = soup.table.tbody.find_all('tr')
            for row in rows:
                elems = row.find_all('td')
                dict_to_add = {}
                dict_to_add['Date'] = day_dt
                for i,elem in enumerate(elems):
                    dict_to_add[columns[i]]=elem.text
                stocks_df = stocks_df.append(dict_to_add, ignore_index=True)
                if len(rows) != 100:
                    print(len(rows)+offset)
                    offset = 0
                    break
                else:
                    offset = offset + 100
    return stocks_df

stocks_df = get_scrapped_week('2020-07-05', '2020-07-11')

stocks_df = stocks_df.append(get_scrapped_week('2020-07-12', '2020-07-18'))
stocks_df = stocks_df.append(get_scrapped_week('2020-07-19', '2020-07-25'))
stocks_df = stocks_df.append(get_scrapped_week('2020-07-26', '2020-08-01'))

filter1 = stocks_df['Surprise(%)']!='-'
filter2 = stocks_df['EPS Estimate']!='-'
filter3 = stocks_df['Reported EPS']!='-'
stocks_df_noMissing = stocks_df[filter1 & filter2 & filter3]
stocks_df_noMissing['EPS Estimate'] = stocks_df_noMissing['EPS Estimate'].astype(float)
stocks_df_noMissing['Reported EPS'] = stocks_df_noMissing['Reported EPS'].astype(float)
stocks_df_noMissing['Surprise(%)'] = stocks_df_noMissing['Surprise(%)'].astype(float)

stocks_df_noMissing.set_index('Symbol')

Получение цен акций для компании

В этом разделе мы рассмотрим, как получить данные о доходности акций и объеме для определенной компании. Для начала вам нужно установить библиотеку yfinance, необходимую для получения данных о акциях:

!pip install yfinance

Давайте начнем с попытки получить одну акцию на одну дату, рассчитав доходность и изменение объема.

import yfinance as yf
import numpy as np
import pandas as pd
from datetime import datetime
from datetime import timedelta

Вот что мы видим для символа FB:

row = stocks_df_noMissing[stocks_df_noMissing['Symbol']=='FB']
print(row)Symbol    Company             Earnings Call Time    EPS Estimate      Reported EPS      Surprise(%) Date776   FB    Facebook, Inc.    Time Not Supplied 1.39          1.8         29.59             2020-07-29

Вы можете легко извлечь дату из этого:

date = row['Date'].values[0]
print(date)2020-07-29

Теперь давайте получим данные о ценах акций для этой даты и близлежащих дат с использованием библиотеки yfinance:

date = datetime.strptime(row['Date'].values[0], '%Y-%m-%d')print(date + timedelta(days=3))
print(date - timedelta(days=1))
ticker = yf.Ticker('FB')
hist = yf.download("FB", start= date - timedelta(days=1), end=date + timedelta(days=3))

Вывод должен выглядеть следующим образом:

2020–08–01 00:00:00
2020–07–28 00:00:00
[*********************100%***********************] 1 of 1 completed

Если вы проверите переменную hist, она должна содержать следующие данные:

На следующем шаге вы определяете рост цены акций и объема за последние два дня наблюдения.

hist['r2'] = np.log(hist['Open'] / hist['Open'].shift(2))hist['volume_rise'] = np.log(hist['Volume'] / hist['Volume'].shift(2))

Таким образом, обновленный набор данных показывает вам рост объема и цены для Facebook:

Если вы хотите посмотреть последнее значение доходности (r2 = доходность за 2 дня на следующее утро после события) в наборе данных, вы можете извлечь его следующим образом:

hist.r2.values[-1]0.10145051589492579

И изменение объема торговли можно просмотреть следующим образом:

hist.volume_rise.values[-1]1.361648662790037

Получение статистики по S&P 500

Как упоминалось в части 2, распространенной практикой является сравнение производительности акций с индексом S&P 500. Для начала давайте получим данные по индексу S&P 500 за указанный период времени:

import pandas_datareader.data as pdr
from datetime import datestart = datetime(2020,7,1)
end = datetime(2020,8,10)print(f’Период 1 месяц до сегодняшнего дня: {start} до {end} ‘)spx_index = pdr.get_data_stooq(‘^SPX’, start, end)# Индекс S&P500 рос почти весь июль 2020 года → необходимо скорректировать рост акций после даты отчетностиspx_index[‘Open’].plot.line()
Рисунок 2: Индекс S&P 500 в июле — августе

Вы можете применить к индексу ту же технику, которую использовали в предыдущем разделе для определения роста цены акций, рассчитав результаты для ежедневного 2-дневного дохода в июле-августе 2020 года:

spx_index[‘r2’] = np.log(np.divide(spx_index[‘Open’] , spx_index[‘Open’].shift(2)))spx_index[‘r2’].plot.line()
Индекс доходности S&P 500 за 2 дня

В табличной форме те же данные будут выглядеть следующим образом:

spx_index.head(30)
Open High Low Close Volume r2 Date2020–08–10 3356.04 3363.29 3335.44 3360.47 2565981272 NaN
2020–08–07 3340.05 3352.54 3328.72 3351.28 2279160879 NaN
2020–08–06 3323.17 3351.03 3318.14 3349.16 2414075395 -0.009843
2020–08–05 3317.37 3330.77 3317.37 3327.77 2452040105 -0.006813
2020–08–04 3289.92 3306.84 3286.37 3306.51 2403695283 -0.010056
2020–08–03 3288.26 3302.73 3284.53 3294.61 2379546705 -0.008814

В следующем фрагменте кода вы заполняете массив доходности S&P для соответствующих акций. Важно отметить, что если есть «пробел» для определенной даты, мы берем ближайшее предыдущее значение:

array_returns_snp500 = []for index,row in stocks_df_noMissing.iterrows():  start_dt = datetime.strptime(row[‘Date’], ‘%Y-%m-%d’) — timedelta(days = 1)  end_dt = datetime.strptime(row[‘Date’], ‘%Y-%m-%d’) + timedelta(days = 3)  # у нас нет пропусков более 4 дней -> попытаемся найти ближайшее значение доходности S&P500 в таблице:
  cur_dt = end_dt
  while cur_dt >= start_dt:
    rez_df = spx_index[cur_dt.strftime(‘%Y-%m-%d’)]
    if len(rez_df)>0:
      array_returns_snp500.append(rez_df.r2.values[0])
      break
    else:
      cur_dt = cur_dt — timedelta(days = 1)

Чтобы убедиться, что все работает как ожидается, вы можете проверить длину обоих наборов данных: нового созданного array_returns_snp500 и ранее введенного stocks_df_noMissing в этой статье:

len(array_returns_snp500)1698len(stocks_df_noMissing)1698

В обоих случаях у вас должно быть одинаковое количество.

Получение доходности акций и объема с Yahoo Finance

В этом разделе мы рассмотрим, как получить данные о доходности акций и объеме для всех тикеров, найденных в Yahoo Finance. В следующем скрипте вы можете рассчитать доходность за 2 дня по открытой цене после отчетности относительно цены 2 дня назад для каждого тикера:

array_tickers = []
array_returns = []
array_volume_rise = []
array_volume_usd = []
array_snp500 = []
for index,row in stocks_df_noMissing.iterrows():
  start_dt = datetime.strptime(row[‘Date’], ‘%Y-%m-%d’) — timedelta(days = 1)
  end_dt = datetime.strptime(row[‘Date’], ‘%Y-%m-%d’) + timedelta(days = 3)
  hist = yf.download(row[‘Symbol’], start = start_dt, end = end_dt)
  
  # Нам нужны полные данные: объем и цена для всех дат для расчета доходности и роста объема
  # ТАКЖЕ: если end_dt - нерабочий день (суббота, воскресенье), мы не можем непосредственно рассчитать статистику доходности
  if len(hist)<4:
    continue
  
  hist[‘r2’] = np.log(np.divide(hist[‘Open’] , hist[‘Open’].shift(2)))
  hist[‘volume_rise’] = np.log(np.divide(hist[‘Volume’], hist[‘Volume’].shift(2)))
  hist[‘volume_usd’] = hist[‘Volume’] * hist[‘Open’]
  
  print(row)
  print(index)
  print(‘ — — — — — — — ‘)
  
  array_tickers.append(row[‘Symbol’])
  array_returns.append(hist.r2.values[-1])
  array_volume_rise.append(hist.volume_rise.values[-1])
  array_volume_usd.append(hist.volume_usd.values[-1])
  
  # Мы добавляем только значения S&P для акций, у которых есть все данные
  array_snp500.append(array_returns_snp500[index])

Скрипт генерирует огромный вывод (приблизительно 1000 записей, если вы помните). Ниже приведен только фрагмент:

[*********************100%***********************] 1 of 1 completedSymbol AEOJF
Company AEON Financial Service Co., Ltd.
Earnings Call Time Time Not Supplied
EPS Estimate 14.03
Reported EPS -5
Surprise(%) -135.67
Date 2020–07–07
Name: 37, dtype: object
37
 — — — — — — — [*********************100%***********************] 1 of 1 completedSymbol BBBY
Company Bed Bath & Beyond Inc.
Earnings Call Time Time Not Supplied
EPS Estimate -1.22
Reported EPS -1.96
Surprise(%) -60.39
Date 2020–07–07
Name: 43, dtype: object
43
 — — — — — — — 

В заключение, было бы интересно узнать, сколько акций имеют следующие финансовые показатели: объем торгов, рост объема (в количестве акций) и доходность:

len(array_tickers)1003

Объединение всех частей вместе

Наконец, давайте объединим все финансовые данные, которые мы получили до сих пор, чтобы увидеть полную "картину" для каждой из торгуемых акций. Для этого мы создадим dataframe:

returns_df = pd.DataFrame(columns=['Тикер', 'Доходность', 'Рост объема', 'Объем торгов в долларах', 'Доходность S&P500'])

И загрузим его данными из созданных нами наборов данных:

returns_df = pd.DataFrame([array_tickers, array_returns, array_volume_rise, array_volume_usd, array_snp500]).transpose()
returns_df.columns = ['Тикер', 'Доходность', 'Рост объема', 'Объем торгов в долларах', 'Доходность S&P500']
returns_df.set_index('Тикер', inplace=True)
returns_df.dropna(inplace=True)
returns_df['Доходность'] = returns_df['Доходность'].astype(float)
returns_df['Рост объема'] = returns_df['Рост объема'].astype(float)
returns_df['Объем торгов в долларах'] = returns_df['Объем торгов в долларах'].astype(float)
returns_df['Доходность S&P500'] = returns_df['Доходность S&P500'].astype(float)
returns_df['Доходность в %'] = np.exp(returns_df['Доходность'])
returns_df['Рост объема в %'] = np.exp(returns_df['Рост объема'])

Также было бы интересно узнать, какие компании имели доходность выше S&P500, и добавить эту информацию в набор данных:

# Доходность выше S&P500
returns_df['Скорректированная доходность'] = returns_df['Доходность'] - returns_df['Доходность S&P500']
returns_df['Скорректированная доходность в %'] = np.exp(returns_df['Скорректированная доходность'])

Метрика "Скорректированная доходность" служит показателем относительного роста по отношению к общему индексу S&P500.

Возможно, вам захочется создать набор гистограмм для каждого столбца в dataframe returns_df, чтобы увидеть представление о распределении данных. Если вы хотите иметь гистограммы без значений INF, вы можете заменить их в dataframe следующим образом:

returns_df = returns_df.replace([np.inf, -np.inf], np.nan)
returns_df.hist(figsize=(20,10), bins=100)
Figure 4: A representation of the distribution of the data in the returns_df dataframe.

На следующем шаге вы объединяете dataframe returns_df с dataframe stocks_df_noMissing:

stocks_and_returns = stocks_df_noMissing.set_index('Symbol').join(returns_df)
stocks_and_returns.head()

Возможно, вам захочется удалить значения INF из итогового набора данных:

stocks_and_returns_no_missing = stocks_and_returns.replace([np.inf, -np.inf], np.nan).dropna()

Вот что у вас должно получиться в итоге:

stocks_and_returns_no_missing.info()
<class 'pandas.core.frame.DataFrame'>
Index: 997 entries, AA to ZEN
Data columns (total 14 columns):
# Column Non-Null Count Dtype
--- --- --- --- --- --- --- --- --- --- --- --- --- -
0 Company 997 non-null object
1 Earnings Call Time 997 non-null object
2 EPS Estimate 997 non-null float64
3 Reported EPS 997 non-null float64
4 Surprise(%) 997 non-null float64
5 Date 997 non-null object
6 Returns 997 non-null float64
7 Volume Rise 997 non-null float64
8 Volume Trade USD 997 non-null float64
9 Returns S&P500 997 non-null float64
10 Returns in % 997 non-null float64
11 Volume Rise in % 997 non-null float64
12 Adj. Returns 997 non-null float64
13 Adj. Returns in % 997 non-null float64
dtypes: float64(11), object(3)
memory usage: 116.8+ KB

Пришло время получить результаты нашего анализа. Какие 50 самых торгуемых акций вокруг даты публикации квартального отчета?

top50_volume = stocks_and_returns_no_missing.sort_values(by='Volume Trade USD', ascending=False).head(50)
print(top50_volume)
Resulting dataset: top stocks returns and volume of trade rise around the quarterly reporting date

Теперь, какие 200 самых торгуемых акций? Запустите этот код, чтобы узнать:

top200_volume = stocks_and_returns_no_missing.sort_values(by='Volume Trade USD', ascending=False).head(200)
print(top200_volume)

Анализ и визуализация

Давайте поиграем с нашим набором данных, пытаясь извлечь интересную информацию из него. В частности, было бы интересно посмотреть на распределение доходности. Чтобы увидеть визуальное подведение итогов информации, мы построим несколько графиков:

На следующем графике вы можете увидеть график столбцов Surprise и Returns в наборе данных top50_volume. Эта информация может быть очень полезной, если вы хотите начать инвестировать в наиболее прибыльные акции.

top50_volume[['Surprise(%)','Returns in %']].plot.scatter(x='Surprise(%)', y='Returns in %')
Figure 5: График Surprise vs. Returns для ТОП 50

Есть несколько наблюдений:

  • большинство акций показывают результат около ожидаемой прибыли на акцию (EPS)
  • 2 выброса Surprise показали 60-кратный и 80-кратный фактический EPS по сравнению с прогнозируемым и небольшую положительную доходность

На следующем графике вы можете увидеть ту же зависимость, но для набора данных top200_volume.

top200_volume[['Surprise(%)','Returns in %']].plot.scatter(x='Surprise(%)', y='Returns in %')
Figure 6: График Surprise vs. Returns для ТОП 200

Несколько замечаний по этому графику:

  • есть несколько новых выбросов с Surprise(%) > 100% и Surprise(%) `< 2000%, которые показали очень впечатляющую доходность 10%-30% всего за 2 дня после объявления квартальных результатов
  • сильное отрицательное отклонение Surprise не означает автоматическое снижение цены: 5 самых левых точек показали значение от -10% до +10% в течение 2 дней, что соответствует -10% до +10% доходности

Что, если "Returns in %" зависит не только от относительного "Surprise in %", но больше от абсолютного значения "Reported EPS"?

На следующем графике мы строим график Surprise и Reported EPS по отношению к Returns in % для ТОП 50. Мы используем подграфики, чтобы показать значения осей:

import matplotlib.pyplot as plt
fig, ax = plt.subplots()
top50_volume[['Surprise(%)','Reported EPS','Returns in %']].plot.scatter(x='Reported EPS', y='Surprise(%)', c='Returns in %', colormap='RdYlGn', ax=ax)

Figure 7: График Surprise и Reported EPS vs. Returns для ТОП 50`

В общем, мы видим, что многие акции сообщают о прибыли на акцию (EPS) от 0 до 2,5 долларов с небольшим положительным отклонением и умеренным ростом (светло-зеленый: до 5% доходности).

На следующем графике вы можете увидеть ту же зависимость, но для ТОП 200:

fig, ax = plt.subplots()
top200_volume[['Surprise(%)','Reported EPS','Returns in %']].plot.scatter(x='Reported EPS', y='Surprise(%)', c='Returns in %', colormap='RdYlGn', ax=ax)
Figure 8: График Surprise и Reported EPS vs. Returns для ТОП 200

На следующем графике мы строим график Surprise и Reported EPS по отношению к Adj. Returns in % для ТОП 50.

fig, ax = plt.subplots()
top50_volume[['Surprise(%)','Reported EPS','Adj. Returns in %']].plot.scatter(x='Reported EPS', y='Surprise(%)', c='Adj. Returns in %', colormap='RdYlGn', ax=ax)
Figure 9: График Surprise и Reported EPS vs. Adj. Returns для ТОП 50

Как видите, Фигура 7 и Фигура 9 не очень отличаются - индивидуальные изменения акций гораздо выше, чем среднее значение S&P500, поэтому Adj. Returns и Returns очень близки.

На следующем графике вы можете увидеть ту же зависимость, но для ТОП 200.

Figure 10: График Surprise и Reported EPS vs. Adj. Returns для ТОП 200

Опять же, Фигура 10 очень похожа на Фигуру 8 (Adjusted Returns и Non-Adjusted Returns).

На следующем графике мы строим график Reported EPS и EPS Estimate по отношению к Returns in % для ТОП 50.

fig, ax = plt.subplots()
top50_volume.plot.scatter(x='Reported EPS', y='EPS Estimate', c='Returns in %', colormap='RdYlGn', ax=ax)
Figure 11: График Reported EPS и EPS Estimate vs. Returns для ТОП 50

На следующем графике мы строим график Reported EPS и EPS Estimate по отношению к Adj. Returns in % для ТОП 50.

Вы увидите похожую картину при построении Adj. Returns vs. Returns:

fig, ax = plt.subplots()
top50_volume.plot.scatter(x='Reported EPS', y='EPS Estimate', c='Adj. Returns in %', colormap='RdYlGn', ax=ax)
Figure 12: График Reported EPS и EPS Estimate vs. Adj. Returns для ТОП 50

На следующей гистограмме мы сравниваем доходность ТОП 50 и ТОП 200 акций с наибольшим объемом торговли. Вы можете заметить, что распределение акций ТОП 200 (синий) имеет более "колоколообразную" форму вокруг 0 и слегка положительной доходности по сравнению с акциями ТОП 50 (оранжевый):

top200_volume['Adj. Returns in %'].hist(bins=50, alpha=0.5)
top50_volume['Adj. Returns in %'].hist(bins=50, alpha = 0.5)
Figure 13: Гистограмма для сравнения доходности ТОП 50 и ТОП 200 акций с наибольшим объемом торговли

Заключение

Мы показали, как парсить финансовые прогнозы с веб-сайта и как связать их с доходностью акций. Второй квартал 2020 года оказался очень успешным для топ-50 акций (по объему торговли) - большинство из них показали положительное отклонение от ожидаемой прибыли на акцию (EPS) и высокую краткосрочную доходность. Результат остается сильным даже после вычета соответствующей доходности индекса S&P500 (т.е. топ-50 акций имели более высокий положительный рост, чем средняя динамика индекса). Когда рассматриваем топ-200 акций, результат становится несколько сложнее - средняя доходность меньше, и есть больше вариации в EPS и доходности.