Вступление
Если вы следите за новостями фондового рынка, вы столкнетесь с такими финансовыми терминами, как выручка, чистая прибыль, прибыль на акцию (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 положительна: инвесторы продолжают покупать растущие акции в течение не только одного дня после объявления. , но на более длительный период времени (одна неделя и более).