Вступление

Если вы следите за новостями фондового рынка, вы столкнетесь с такими финансовыми терминами, как выручка, чистая прибыль, прибыль на акцию (EPS). Они часто появляются сразу после ежеквартальных и годовых отчетов о прибылях и убытках. Эти показатели отражают непосредственные операционные результаты компании, и аналитики используют их для расчета долгосрочной тенденции прибыльности, которая выражается в справедливой цене акций.

В этой статье мы более подробно рассмотрим историческую прибыль на акцию для ~ 200 компаний, выбрав наиболее активные акции в течение последнего торгового дня (которые обычно включают крупнейшие «голубые фишки» и некоторые другие менее популярные компании). Мы получаем данные о ценах на акции примерно в даты ежеквартальных отчетов и рассчитываем повышение или понижение цен сразу после объявления результатов. Основная идея состоит в том, чтобы проверить долгосрочные результаты прогнозируемой прибыли на акцию по сравнению с фактической прибылью на акцию и ее влияние на курс акций.

В ходе этой статьи мы постараемся найти ответы на следующие вопросы:

  • Все ли компании стараются отчитываться очень близко к оценкам? Что является достаточно большим сюрпризом (%), чтобы вызвать краткосрочный шок на курсах акций?
  • Если скачок вызвал быстрое изменение цены акции, сохраняется ли он в течение нескольких дней, и можем ли мы предсказать направление его движения (чтобы можно было использовать эти знания для краткосрочных инвестиций)?
  • Приносит ли последовательная отчетность о легком позитивном сюрпризе к долгосрочному росту акций?
  • Увеличивает ли постепенное увеличение прибыли на акцию оценку компании?

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

Эта статья - пятая часть серии. Предыдущая часть 4 была введением в тему: она показывала, как очистить одну страницу с financial.yahoo.com о прибыли на акцию и сосредоточилась на одном отчетном периоде (отчеты о прибылях за август 20, охватывающие второй квартал) 20 доходов). Эта часть гораздо более продвинутая: она берет всю доступную историю для выбранного набора акций и направлена ​​на поиск инвестиционных возможностей, а не на более простой исследовательский анализ.

Этапы подготовки

Как и в предыдущих частях этой серии, мы будем использовать записную книжку Google Colab (colab.research.google.com) для реализации кода Python, необходимого для исследования.

Для начала нам понадобится стандартный набор аналитического, списываемого и финансового импорта:

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

Затем мы создаем набор тикеров для использования в нашем анализе:

ADDITIONAL_TICKERS = [‘CAT’, ‘SNAP’, ‘TSLA’, ‘GE’, ’GOOG’, ‘FB’, ‘AMD’, ’ATVI’, ’SAP’, ’EBAY’]

Функции для парсинга

Поскольку мы собираемся получить некоторые данные для анализа путем парсинга, нам нужно создать для этого код. Повторно используя подход из Части 4 по удалению одной страницы на одну таблицу и ее очистке (нам нужно очистить недостающие значения), мы создаем функцию get_scraped_yahoo_finance_page (), которая работает следующим образом:

  • Принимает URL и URL_PARAM в качестве входных аргументов
  • Находит одну существующую таблицу в таблице HTML
  • Сохраняет ману столбца таблицы (есть один хак для самой активной страницы, так как один столбец не виден)
  • Построчно извлекает значения
  • Возвращает фрейм данных со всеми полями как объект

Нам также необходимо создать clean_earnings_history_df (), который удаляет все ячейки с пропущенными значениями («-»)

Вот реализация этих функций:

# Example of a full URL with one stock Symbol
# url = “https://finance.yahoo.com/calendar/earnings?symbol=ATVI"
def get_scraped_yahoo_finance_page(url, url_params):
  # url = “https://finance.yahoo.com/calendar/earnings"
  # url_params = {‘symbol’:ticker}
  r = requests.get(url, params=url_params)
  soup = BeautifulSoup(r.text)
  table = soup.find_all(‘table’)
  # DEBUG: Just 1 table found which is good
  print(‘We\’ve found tables:’, len(table))
  if len(table)!=1:
    return None
  # Get all column names
  if len(soup.table.find_all(‘thead’))==0:
    return None
  spans = soup.table.thead.find_all(‘span’)
  columns = []
  for span in spans:
    columns.append(span.text)
    # Hack: due to some reason one of the columns are not <span> tag   for calendar/earnings and is not discovered in the columns list. 
    # we manually add this column 
  if url.find(‘https://finance.yahoo.com/most-active') != -1 :
    columns.insert(len(columns)-2,”Market Cap”)
  rows = soup.table.tbody.find_all(‘tr’) 
  # read row by row
  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) 
  return stocks_df
# The only record per stock appears with this pattern : the next earnings date
def get_next_earnings_records(stocks_df):
  filter1 = stocks_df[‘EPS Estimate’]!=’-’
  filter2 = stocks_df[‘Surprise(%)’]==’-’
  filter3 = stocks_df[‘Reported EPS’]==’-’
  rez_df = stocks_df[filter1 & filter2 & filter3]
  return rez_df
# Remove all records with not filled stats and cast to float values
def clean_earnings_history_df(stocks_df):
  filter1 = stocks_df[‘EPS Estimate’]!=’-’
  filter2 = stocks_df[‘Surprise(%)’]!=’-’
  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)
  return stocks_df_noMissing

Давайте теперь опробуем вышеуказанные функции и получим статистику для F (Ford):

ticker = 'F'
f_df = get_scraped_yahoo_finance_page(url = "https://finance.yahoo.com/calendar/earnings",url_params = {'symbol':ticker})
f_df_clean = clean_earnings_history_df(f_df)

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

Получение 200 лучших торгуемых акций

Здесь мы рассмотрим 200 наиболее активных акций за последний торговый день (середина ноября 2020 г.). Список (вместе с выбранными акциями) будет использоваться для получения исторических значений EPS и будущих доходов на даты EPS (для проверки гипотезы, если хорошая или плохая EPS предсказывает хорошие доходы). Мы применим подход регулярных выражений для преобразования всех текстовых значений в числовые символы (значения% и величина M, B, T).

num_stocks = 200
most_active_stocks = get_scraped_yahoo_finance_page(url = “https://finance.yahoo.com/most-active",url_params = {‘count’:num_stocks})

Проблема в том, что все значения в фрейме данных являются объектами. Не целые числа или числа с плавающей запятой, то есть вы не можете выполнять с ними арифметические операции.

import re
POWERS = {‘T’: 10 ** 12,’B’: 10 ** 9, ‘M’: 10 ** 6, ‘%’: 0.01, ‘1’:1}
“””Read a string (with M/B/T/% values)
 Return a correct numeric value
“””
def convert_str_to_num(num_str):
  match = re.search(r”([0–9\.-]+)(M|B|T|%)?”, num_str)
  if match is None:
    return None
  else: 
    quantity = match.group(1)
    if match.group(2) is None:
      magnitude = ‘1’ # no letter in the end->don’t change the value
   else: 
     magnitude = match.group(2)
     # print(quantity, magnitude)
   return float(quantity) * POWERS[magnitude]

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

columns_to_apply = [ ‘Price (Intraday)’, ‘Change’, ‘% Change’, ‘Volume’, ‘Avg Vol (3 month)’, ‘Market Cap’,
 ‘PE Ratio (TTM)’]
for col in columns_to_apply:
  most_active_stocks[col] = most_active_stocks[col].apply(convert_str_to_num)

В результате мы можем применить математику к фрейму данных:

most_active_stocks[“log_market_cap”] = np.log10(most_active_stocks[“Market Cap”])

И построим гистограмму:

Теперь разделим активные акции на 3 группы одинакового размера:

most_active_stocks[“log_market_cap_binned”] = pd.qcut(most_active_stocks.log_market_cap,3)

В итоге мы имеем 3 примерно равные группы по ложе 66–67 размеров:

Теперь мы можем найти средние значения для этих групп:
1)% Изменение: самые большие запасы изменяются на -0,4%, а меньшие запасы на 1,3%
2) Общий объем для кластеров может быть сопоставим (то же самое степень 10), но рыночная капитализация различается в 5–10 раз (6,4 * 10⁹ против 2,7 * 10¹⁰ против 2,17 * 10¹¹)
3) Коэффициент P / E (если указан): высокий для крупнейших акций ( 78), средний - для маленьких (51) и самый высокий - для маленьких (104)

Вы также можете удалить выбросы: top-xx% с каждой стороны. Это можно реализовать с помощью следующей функции:

def remove_outliers(df, column_name, quantile_threshold):
  q_low = df[column_name].quantile(quantile_threshold)
  q_hi = df[column_name].quantile(1-quantile_threshold)
  rez = df[(df[column_name] < q_hi) & (df[column_name] > q_low)]
return rez

А затем используйте его следующим образом:

tmp = remove_outliers(df = most_active_stocks, column_name = “% Change”,quantile_threshold = 0.02)
tmp[“% Change”].hist(bins=100)

tmp = remove_outliers(df = most_active_stocks, column_name = “Change”,quantile_threshold = 0.02)
# The difference vs. previous graph: we draw the abs. daily change here vs. relative change in % in the previous graph
tmp[“Change”].hist(bins=100)

# We want to get some idea of the stock was traded today vs. 3-month average volume
most_active_stocks[“relative_volume”] = most_active_stocks[“Volume”]/most_active_stocks[“Avg Vol (3 month)”]

Давайте подведем итоги того, что у нас есть на данный момент, и перечислим наши выводы:

[[РЕЗУЛЬТАТ 1]]: мы разделяем компании на средние, крупные и крупнейшие (по log_market_cap_binned). В большинстве случаев изменение в первый день составляет от -5% до + 5%, в то время как средние и малые компании могут упасть / подняться еще дальше до -10 / + 10%. Это всего лишь один торговый день, но он может дать общее представление о том, как акции могут изменить свою стоимость за один день. Объемы торгов крупнейших компаний редко превышают средний объем торгов за 3 месяца более чем в 3 раза, в то время как у более мелких компаний объемы торгов могут достигать 5–6 раз.

Давайте построим диаграмму, чтобы проиллюстрировать это:

import seaborn as sns
sns.set(rc={‘figure.figsize’:(10,6)})
sns.kdeplot(
 data = most_active_stocks, x=”% Change”, y=”relative_volume”, hue=”log_market_cap_binned”, fill=True,
)

Чем ниже рыночная капитализация акции (голубой цвет), тем больше потенциальное изменение в% (диапазон синей формы на горизонтальной оси) и относительный объем (диапазон синей формы на вертикальной оси).

[[РЕЗУЛЬТАТ 2]] Средние и крупные компании, как правило, имеют более отрицательный и положительный диапазон доходности и образуют «более широкий» колокол - с более высоким стандартным отклонением от среднего. Как инвестор, вы можете заработать больше с небольшими компаниями, но на самом деле рискуете и больше. Таким образом, доходность на риск может быть любой: меньше или больше для средних-крупных-крупнейших акций.

sns.set(rc={‘figure.figsize’:(10,6)})
sns.kdeplot(
 data = most_active_stocks, x=”% Change”, hue=”log_market_cap_binned”,
 fill=True, common_norm=False, 
 # palette=”rocket”,
 alpha=.5, 
 linewidth=0,
)

Получение фрейма данных со всей историей на акцию на акцию по наиболее торгуемым акциям

Мы загрузили все доступные даты для EPS (прибыли на акцию) для наиболее торгуемых акций:

Также есть тикеры в списке ADDITIONAL_TICKERS, определенном в начале этой статьи. Эти тикеры могут не отображаться в списке наиболее торгуемых акций:

NEW_TICKERS = [x for x in ADDITIONAL_TICKERS if x not in set(most_active_stocks.Symbol)]
TICKERS_LIST = most_active_stocks.Symbol.append(pd.Series(NEW_TICKERS))

В следующем коде мы собираем информацию для каждого тикера с https://finance.yahoo.com/calendar/earnings.

from random import randint
from time import sleep
# Empty dataframe
all_tickers_info = pd.DataFrame({‘A’ : []})
for i,ticker in enumerate(TICKERS_LIST):
  current_ticker_info = get_scraped_yahoo_finance_page(url = “https://finance.yahoo.com/calendar/earnings",url_params = {‘symbol’:ticker})
  print(f’Finished with ticker {ticker}, record no {i}’)
  if all_tickers_info.empty:
  all_tickers_info = current_ticker_info
  else:
   all_tickers_info =    pd.concat([all_tickers_info,current_ticker_info], ignore_index=True) 
 # Random sleep 1–3 sec
  sleep(randint(1,3))

Прежде чем продолжить, нам нужно рассчитать ближайшие даты будущих доходов:

next_earnings_dates = get_next_earnings_records(all_tickers_info)

Мы знаем, когда наступит следующий отчетный день, и хотим знать, чего ожидать от ближайших совещаний по анализу отчетов?

next_earnings_dates.sort_values(by=’Earnings Date’).tail(30)

На следующем этапе мы удаляем все ячейки с пропущенными значениями («-»):

all_tickers_info_clean = clean_earnings_history_df(all_tickers_info)

Теперь давайте проверим, что у нас есть на агрегированном уровне:

[[РЕЗУЛЬТАТ 3]]: средняя оценка EPS составляет всего 0,52, что недалеко от реальности. 0,50 = ›это всего лишь 1,2% сюрприз в среднем случае. Стандартное отклонение для сюрприза составляет 386% - это говорит о том, что в наборе данных много выбросов:
- небольшие значения EPS (‹0,13), как правило, занижают отчет по EPS (- 1% сюрприз),
- средний (EPS = 0,38), немного завышенный на 4% выше оценки,
- самый высокий квантиль (EPS ›0,75), в котором они превышают в среднем 13%

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

all_tickers_info_clean[[‘EPS Estimate’,’Reported EPS’]].plot.box()

Если мы удалим 360 записей (из 12636 значений) с экстремальными значениями (абс. Z-оценка ›2) - тогда мы сможем лучше рассмотреть коробчатую диаграмму: останется меньше выбросов в обоих направлениях:

import scipy
# calculate z-scores of `df`
z_scores = scipy.stats.zscore(all_tickers_info_clean[[‘EPS Estimate’,’Reported EPS’]])
abs_z_scores = np.abs(z_scores)
filtered_entries = (abs_z_scores < 2).all(axis=1)
new_df = all_tickers_info_clean[[‘EPS Estimate’,’Reported EPS’]][filtered_entries]
new_df.plot.box()

Теперь нам нужно преобразовать даты во фрейме данных, чтобы упростить дальнейший анализ:

from datetime import datetime 
from datetime import timedelta
all_tickers_info_clean[‘Earnings Date 2’] = all_tickers_info_clean[‘Earnings Date’].apply(lambda x:datetime.strptime(x[:-3], ‘%b %d, %Y, %H %p’) )

Затем мы можем сгенерировать ПЕРВИЧНЫЙ КЛЮЧ (PK), который впоследствии будет использоваться в операциях слияния, используя строку даты заработка без времени и тикера.

all_tickers_info_clean[‘PK’] = all_tickers_info_clean.Symbol + “|”+ all_tickers_info_clean[“Earnings Date 2”].apply(lambda x : x.strftime(‘%Y-%m-%d’))

Получение всей доступной истории цен на акции для выбранных тикеров

Если вы работаете в Colab, начните с установки yfinance в свой ноутбук:

!pip install yfinance
import yfinance as yf

В следующем коде мы генерируем таблицу с дневными ценами и будущими доходами (через 1–7,30,90,360 дней):

# Start from an empty dataframe
df_stocks_prices = pd.DataFrame({‘A’ : []})
# Download all history of stock prices and calculate the future returns for 1–7 days, 30d, 90d, 365d 
# That is: we are very interested if we buy stock at some date (e.g. high EPS) -> if it is going to be a profitable decision
for i,ticker in enumerate(TICKERS_LIST):
 yf_ticker = yf.Ticker(ticker)
 historyPrices = yf_ticker.history(period=’max’)
 historyPrices[‘Ticker’] = ticker
 # Sometimes there is a problem with .index value → use try
 # https://stackoverflow.com/questions/610883/how-to-know-if-an-object-has-an-attribute-in-python
 try:
 historyPrices[‘Year’]= historyPrices.index.year
 historyPrices[‘Month’] = historyPrices.index.month
 historyPrices[‘Weekday’] = historyPrices.index.weekday
 historyPrices[‘Date’] = historyPrices.index.date
 except AttributeError:
 print(historyPrices.index)
 # !!! Important: we do historyPrices[‘Close’].shift(1) — to get the Close market price 1 day BEFORE current
 # !!! Important: we do historyPrices[‘Close’].shift(-i)) — to get the Close market price the i days AFTER current
 # If you divide second on first -> you get the returns from holding “i days” the stock that you bought the day before financial reporting occurred
 for i in [1,2,3,4,5,6,7,30,90,365]:
 historyPrices[‘r_future’+str(i)] = np.log(historyPrices[‘Close’].shift(-i) / historyPrices[‘Close’].shift(1) )
historyPrices[‘years_from_now’] = historyPrices[‘Year’].max()- historyPrices[‘Year’]
 historyPrices[‘ln_volume’]= np.log(historyPrices[‘Volume’])
 if df_stocks_prices.empty:
 df_stocks_prices = historyPrices
 else: 
 df_stocks_prices = pd.concat([df_stocks_prices,historyPrices], ignore_index=True)

Мы генерируем тот же ПЕРВИЧНЫЙ КЛЮЧ для (внутреннего) соединения с фреймом данных EPS: ‹Symbol | Дата>:

df_stocks_prices[“PK”] = df_stocks_prices.Ticker + “|”+ df_stocks_prices[“Date”].apply(lambda x : x.strftime(‘%Y-%m-%d’))

Мы создали множество ежедневных статистических данных (1,2 миллиона записей для 200 акций) о финансовых показателях выбранных акций.

Давайте теперь посмотрим на пример того, как рассчитываются r_future1 и r_future2 для одной акции. Для этого:
• выберите вторую строку для даты «2020–10–02»: цена закрытия GE для 2020–10–01 составляла 6,24, для 10–05 (следующий торговый день после 10–02). было 6,41, за 10–06 было 6,17
• r_future1 = log (6,41 / 6,24) = log (1,027) = 0,026 - это примерно 2,6% прибыли от покупки акций GE 1 октября и их продажи 5 октября ( Через 1 торговый день после отчетной даты)
• r_future2 = log (6,17 / 6,24) = log (0,988) = -0,011 - это примерно -1,1% прибыли (убытка) от покупки акций GE 1 октября и продажи 6 октября (2 торговых дня после отчетной даты)

filter1 = df_stocks_prices.Ticker==’GE’
filter2 = df_stocks_prices.Year == 2020
filter3 = df_stocks_prices.Month == 10
df_stocks_prices[filter1 & filter2 & filter3].head(2)

У нас все еще могут быть дубликаты в наборе данных all_tickers_info_clean из-за двойных записей на исходном веб-сайте:

all_tickers_info_clean = all_tickers_info_clean.drop_duplicates(subset=[‘PK’], keep=’first’)

Нам также необходимо удалить дубликаты из df_stocks_prices, если они у нас есть:

df_stocks_prices = df_stocks_prices.drop_duplicates(subset=[‘PK’], keep=’first’)

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

merged_df = pd.merge(all_tickers_info_clean, df_stocks_prices, on=”PK”, validate=”one_to_one”)

Результирующий фрейм данных должен иметь следующую структуру:

Вернулся к нормальному размеру 12 тыс. Строк, потому что мы храним только записи за отчетные дни, уменьшив объем в 100 раз: с 1,2 млн до 12 тыс. Строк.

Примеры отдельных акций: недавние всплески в отчетах за 3 и 2 квартал

В частности, мы рассмотрим следующие тикеты, взяв соответствующие индикаторы из фрейма данных merged_df:

• Акции [GE] подскочили после того, как компания неожиданно сообщила о скорректированной прибыли за третий квартал, выручка превысила ожидания (https://www.cnbc.com/2020/10/28/general-electric-ge- earnings-q3-2020.html )

[MSFT] Акции Microsoft выросли после того, как компания сообщила о 15-процентном скачке продаж и сообщила, что коронавирус минимально повлиял на выручку ( https://www.cnbc.com/2020/04/29/microsoft-msft-earnings-q3-2020.html#:~:text=Earnings%3A%20%241.40%20per%20share%2C%20adjusted,Revenue % 3A% 20% 2435.02% 20млрд )

Для проведения анализа воспользуемся следующей функцией:

def draw_plot(symbol):
  filter = (merged_df.Symbol== symbol) & (merged_df.Year>=2010)
  df = merged_df[filter][[“EPS Estimate”,”Reported EPS”,”Surprise(%)”,”r_future1",”r_future7",”Date”]]
  with pd.option_context(‘display.max_rows’, None, ‘display.max_columns’, None): # more options can be specified also
  
  print(df.head(15))
  
  #Graph1: EPS estimate vs. Reported EPS 
  df[[“EPS Estimate”,”Reported EPS”,”Date”]].plot.line(x=”Date”, figsize=(20,6), title=” EPS Estimate vs. Reported”)
  
  #Graph2: Surprise in %
  df[[“Surprise(%)”,”Date”]].plot.line(x=”Date”, figsize=(20,6), title=”Surprise % (=Reported EPS/EPS Estimate)”)
  #Graph3: 1- and 7-days returns
df[[“r_future1”,”r_future7",”Date”]].sort_values(by=’Date’).plot(x=”Date”, kind=’bar’, figsize=(20,6), title=”Stock jump”)

Давайте взглянем на акции GE:

  • В большинстве периодов отчетная прибыль на акцию выше прогнозируемой.
  • Четыре точки данных (квартала) показывают отрицательный сюрприз, который не вызывал большого шока для курса акций в первый раз, но вызывал падение на 10–20% в других случаях.
  • Последний отчет за третий квартал 2020 года показал положительную динамику: прибыль на акцию изменилась с отрицательного значения на положительное, что привело к росту на 3,7% в первый день и 13% -ному росту акций за 7 дней.
  • Результаты последнего квартала представляют собой следующую инвестиционную идею: если Сюрприз (фактическая прибыль на акцию против прогнозируемой прибыли на акцию) будет положительным и большим, то акция может подскочить за один день (r1_future) и продолжить свой рост в течение всей недели после этого. (r7_future). Инвестор может отслеживать такие случаи и покупать акции сразу после очень удачной отчетной даты, стремясь продать их в короткие сроки.
draw_plot(“GE”)

draw_plot(“MSFT”)

Давайте посмотрим на акции MSFT:

  • Microsoft (MSFT), как правило, имеет фактическую прибыль на акцию, отличную от оценок, на -10% .. + 20%.
  • В конце 2017 года была одна дата с неожиданными 40%, которая действительно вызвала положительный всплеск доходности, хотя r1 и r7 не сильно различаются (что, вероятно, произошло из-за низкого абсолютного значения EPS <0,01).
  • Последние 6 кварталов акции демонстрировали тенденцию к постепенному увеличению прибыли на акцию, при этом r7_future всегда было выше, чем r1_future. Это означает, что акции MSFT были хорошей возможностью для инвестиций в течение всех кварталов последних 1,5 лет.
  • Инвестиционная идея: попробуйте найти акции с растущей прибылью на акцию в течение 1-2 лет и инвестируйте в них примерно в отчетный день.

Масштабированный анализ

В этом разделе мы рассмотрим агрегированные статистические данные по многолетним данным и различным параметрам для группировки.

Первый пример - это доходность за 1 и 7 дней для всех акций, сгруппированных по годам.

import matplotlib.pyplot as plt
print(‘Count observations: ‘,merged_df.groupby(by=’Year’).count()[‘r_future1’])
ax = merged_df.groupby(by=’Year’).mean()[[‘r_future1’,’r_future7']].plot.line(figsize=(20,6))
vals = ax.get_yticks()
ax.set_yticklabels([‘{:,.1%}’.format(x) for x in vals])
plt.axhline(y=0, color=’r’, linestyle=’-’)
plt.title(“1 and 7 days returns of stocks after the quarterly earnings results announcement”)

[[РЕЗУЛЬТАТ 4]] В течение многих лет (но не всегда!) ожидаемая прибыль через 7 дней выше, чем через 1 день с отчетной даты. Это означает, что отдельные тенденции, которые мы наблюдали ранее для MSFT и GE, как правило, обычно используются для более крупного набора данных, но только в течение успешных «бычьих» лет (когда r_future1 ›0 в среднем).

Параметризация функции (с условиями фильтрации и группировки)

Здесь мы создаем параметризованную версию предыдущего графика, в которой вы можете выбрать функцию для groupby и условие для фильтрации.

def draw_returns(groupby_factor, filter): 
  filter_year = merged_df.Year>=2000
  print(‘Count observations: ‘,merged_df[filter & filter_year].groupby(by=groupby_factor).count()[‘r_future1’])
  ax = merged_df[filter & filter_year].groupby(by=groupby_factor).mean()[[‘r_future1’,’r_future7']].plot.line(figsize=(20,6))
 vals = ax.get_yticks()
  ax.set_yticklabels([‘{:,.0%}’.format(x) for x in vals])
  plt.axhline(y=0, color=’r’, linestyle=’-’)
  plt.title(“1 and 7 days returns of stocks after the quarterly earnings results announcement”)
  if groupby_factor==’Year’:
    ax.xaxis.set_major_locator(MaxNLocator(integer=True))

[[РЕЗУЛЬТАТ 5]] Мы пробуем параметризованный подход, получая доходность r1 и r7 для разных классов акций (EPS ‹-1, EPS‹ 0, EPS ›0, EPS› 1, EPS ›2 так далее.). Мы хотим выяснить, что одна линия (r7) всегда выше, чем другая (r1), но, к сожалению, не можем доказать, что это правда.

draw_returns(‘Year’, merged_df[“Reported EPS”]<0)
draw_returns(‘Year’, merged_df[“Reported EPS”]<-1)
draw_returns(‘Year’, merged_df[“Reported EPS”]>0)
draw_returns(‘Year’, merged_df[“Reported EPS”]>1)
draw_returns(‘Year’, merged_df[“Reported EPS”]>2)

Разделение данных об объеме торговли

Вы также можете попытаться разрезать данные об объеме торговли / размере компании и посмотреть, есть ли какое-нибудь впечатляющее поведение для какой-либо из подгруппы.

merged_df.ln_volume.replace([np.inf, -np.inf], np.nan).hist(bins=100)

Создаем 10 бинов одинакового размера:

merged_df[“ln_volume_binned”] = pd.qcut(merged_df[“ln_volume”],10)
merged_df.ln_volume_binned.value_counts()
(17.674, 21.613] 1226 
(6.396, 14.344] 1226 
(17.111, 17.674] 1225 
(16.74, 17.111] 1225 
(16.436, 16.74] 1225 
(16.157, 16.436] 1225 
(15.85, 16.157] 1225 
(15.49, 15.85] 1225 
(15.037, 15.49] 1225 
(14.344, 15.037] 1225 Name: ln_volume_binned, dtype: int64

Теперь мы можем использовать тот же параметризованный подход для разделения данных на ln_volume_binned и выбора различных фильтров (например, merged_df [«Год»] ›2000 (Рис.30),
merged_df [« Год »] == 2020 (Рис.31) ),
(merged_df [«Год»] == 2020) & (merged_df [«Отчетная прибыль на акцию»] ›0) (Рис.32):

draw_returns(‘ln_volume_binned’, merged_df[“Year”]>2000)
draw_returns(‘ln_volume_binned’, merged_df[“Year”]==2020)
draw_returns(‘ln_volume_binned’, (merged_df[“Year”]==2020) & (merged_df[“Reported EPS”]<0))

Первый график показывает, что средние акции с большим объемом торговли были отрицательными по доходности в среднем за период 2000–2020 годов. В 2020 году тенденция изменилась: крупные акции имеют положительную доходность, а r7 выше, чем r1. Если мы посмотрим глубже в 2020 году и добавим условие EPS ‹0, то мы увидим большую положительную разницу для доходностей r7 и r1 (что хорошо, если вы покупаете в день 1 и продаете в день 7).

[[РЕЗУЛЬТАТ 6]] Мы попытались разделить акции по объему торгов, году и прибыли на акцию. Универсальных трендов (когда одна линия r7 лежит над другой линией r1) нет, но мало (более слабых) наблюдений сохраняется. В 2020 году: акции с большим объемом торгов, как правило, имеют положительную краткосрочную доходность (в отличие от предыдущих лет), а акции с отрицательной прибылью на акцию, как правило, быстрее возвращаются к предыдущим (до отчетности) ценам и растут. помимо этого.

Анализ неожиданности в%

Сюрприз в основном сосредоточен вокруг 0, так как многие компании хотят сообщать очень близкое значение:

merged_df[“surprise_%_binned”] = pd.qcut(merged_df[“Surprise(%)”],10)
merged_df[“surprise_%_binned”].sort_values().value_counts()
(-31360.231, -17.356] 1226
(-17.356, -3.39] 1226
(-3.39, 0.27] 1229 
(0.27, 1.87] 1221 
(1.87, 3.73] 1226 
(3.73, 6.446] 1223 
(6.446, 10.677] 1225
(10.677, 17.52] 1226 
(17.52, 36.469] 1224
(36.469, 6900.0] 1226 
Name: surprise_%_binned, dtype: int64

Давайте подведем итоги:

draw_returns(‘surprise_%_binned’, True)

[[РЕЗУЛЬТАТ 7]] Общее правило состоит в том, что если Сюрприз (%) отрицательный, то r1 и r7 также отрицательны. Если сюрприз положительный - r1 и r7 тоже положительны. Сложно использовать фактор неожиданности (%) в качестве инвестиционной идеи, поскольку линии r1 и r7 лежат близко друг к другу. Кажется, что они расходятся, начиная с высокого положительного сюрприза (%) ›17. Среднее количество таких случаев составляет 20% (два самых высоких диапазона из десяти), что означает, что инвестору необходимо отслеживать множество дат отчетов о прибылях и убытках, чтобы выявить самые высокие положительные случаи.

Заключение

Мы использовали различные бесплатные источники данных, чтобы создать фреймворк со статистикой около 200 крупнейших (по размеру и объему торгов) компаний, торгуемых на фондовом рынке США.

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

Результаты исследования (из выделенных [[РЕЗУЛЬТАТ1-РЕЗУЛЬТАТ7]] абзацев текста):

  • Изменение цены акции за 1 день около отчетной даты находится в интервале [-5% .. + 5%]. Акции меньшего размера, как правило, имеют большие скачки (и потенциальную прибыль для инвестора), но они также несут больший риск;
  • Средняя заявленная прибыль на акцию составляет 0,52 доллара, что близко к расчетной цене на акцию в 0,50 доллара. В большинстве наблюдений разница между этими значениями составляет всего несколько процентов (во многих случаях Сюрприз (%)
  • Есть некоторые долгосрочные тенденции для отдельных акций (таких как GE и MSFT), когда постепенное увеличение EPS радует инвесторов и повышает цены r1 и r7 на несколько кварталов подряд. Эти тенденции можно обобщить на «успешные» годы, когда среднее r1_future ›0 и среднее r7_future› 0;
  • Трудно найти устойчивый кластер акций (с некоторыми условиями для EPS, Surprise, года, объема и т. Д.), Который будет иметь безрисковую прибыль (доходность через 7 дней выше, чем через 1 день: r7 ›r1) ;
  • Существуют определенные пороговые значения в районе - + 10% для неожиданности (%), которые вызывают массовые продажи или покупки со стороны инвесторов и перемещают цены в отрицательную или положительную сторону. Если Сюрприз (%) превышает 17% (~ 20% всех точек данных за 20 лет), то средняя разница между r7 и r1 положительна: инвесторы продолжают покупать растущие акции в течение не только одного дня после объявления. , но на более длительный период времени (одна неделя и более).