Введение
На фондовом рынке никогда не прекращаются поиски новой эффективной стратегии, способной принести максимальную прибыль при минимальных рисках. После экспериментов с различными техническими индикаторами и сложными алгоритмами я заинтересовался более фундаментальным подходом, сфокусированном на базовых финансовых показателях компаний, а не только на изменении курсов акций.
Так я обнаружил, что с помощью самой простой стратегии, использующей всего два базовых бизнес-показателя (P/B и ROE), можно выявить недооцененные, но высокоприбыльные акции. Результаты моих исследований были впечатляющими. Простая стратегия оказалась настолько эффективной, что позволила быстро обыграть рынок. В этой статье я расскажу, как реализовать такую стратегию, используя Python и получая данные из API EODHD.
Значение коэффициентов P/B и ROE
Прежде чем погрузиться в код и бэктесты, важно понять два ключевых показателя, составляющих основу этой стратегии.
1. P/B (price-to-book — капитализация/балансовая стоимость)
Коэффициент P/B сопоставляет текущую рыночную капитализацию компании с ее балансовой стоимостью. Низкий коэффициент P/B может указывать на недооцененность акций, а может сигнализировать о проблемах с фундаментальными показателями компании. Однако в сочетании с высоким ROE он часто указывает на недооцененные компании с надежными финансовыми показателями.
2. ROE (return on equity — рентабельность собственного капитала)
Коэффициент ROE выражает прибыльность компании по отношению к ее собственному капиталу. Высокий ROE указывает на то, что компания приносит значительную прибыль на вложенный акционерами капитал, что является положительным сигналом для инвесторов.
Механика торговой стратегии
Основная идея стратегии заключается в выявлении компаний, которые потенциально недооценены рынком, но при этом способны генерировать высокую прибыль на капитал. Механика стратегии проста:
- Отбор акций: отбираем акции в рамках S&P 500 с коэффициентом P/B менее 2 и ROE более 15 %.
- Инвестиционный тезис: отобранные акции, скорее всего, недооценены, но при этом эффективно генерируют прибыль. Инвестируя в эти акции, ожидаем, что рынок в конечном итоге исправит ценовое несоответствие, и это приведет к росту цен акций.
- Логика торговли: занимаем длинную позицию по отношению к этим отобранным акциях и следим за динамикой портфеля с течением времени.
Реализация торговой стратегии
А теперь займемся практическим делом — реализуем стратегию на Python!
Шаг 1. Импорт пакетов
Первый и самый важный этап — импортирование всех необходимых пакетов в среду Python. В данном случае будем использовать только три пакета:
- Pandas — для форматирования, очистки, манипулирования данными и других связанных с этим целей;
- Matplotlib — для создания графиков и различных видов визуализации;
- Requests — для выполнения вызовов API с целью извлечения данных.
Следующий код импортирует все перечисленные пакеты в среду Python:
# Импортирование пакетов
import pandas as pd
import requests
import matplotlib.pyplot as plt
Если вы не установили ни один из импортированных пакетов, обязательно сделайте это с помощью команды pip
в терминале.
Шаг 2. Извлечение тикеров S&P 500
Мы будем выбирать акции на основе критериев отбора из индекса S&P 500. Для этого сначала понадобится список акций, входящих в индекс. Прошли те времена, когда тикеры извлекались со страницы Википедии с помощью веб-скрейпинга — теперь есть более доступные и разнообразные API. Следующий код извлекает список доступных в индексе тикеров, используя API фундаментальных данных EODHD:
api_key = '<YOUR API KEY>'
sp500_json = requests.get(f'https://eodhd.com/api/fundamentals/GSPC.INDX?api_token={api_key}&fmt=json').json()
stocks = []
for i in range(len(sp500_json['Components'])):
ticker = sp500_json['Components'][f'{i}']
stocks.append(ticker)
print(f"Total S&P 500 stocks: {len(stocks)}")
Начнем с определения API-ключа (обязательно замените <YOUR EODHD API KEY>
на свой секретный API-ключ, который получите при регистрации на EODHD). Затем создаем API-запрос для получения фундаментальных данных самого индекса S&P 500, представленного тикером GSPC.INDX
.
Ответ в JSON-формате от API содержит поле «Components» (Компоненты) со списком всех тикеров S&P 500. Проходим по этому списку, извлекая каждый тикер и добавляя его в список stocks
.
Наконец, выводим общее количество полученных акций S&P 500. Список полон и готов к дальнейшей обработке в рамках стратегии.
Шаг 3: Извлечение фундаментальных данных
На этом этапе извлечем необходимые фундаментальные данные для каждой акции из списка S&P 500 с помощью конечной точки API фундаментальных данных EODHD. В частности, нас интересуют P/B и ROE — ключевые показатели для данной стратегии.
# Извлечение фундаментальных данных для акций компаний из S&P 500
api_key = '<YOUR EODHD API KEY>'
fundamentals_df = pd.DataFrame(columns = ['stock', 'pb', 'roe'])
fundamentals_df['stock'] = stocks
for i in range(len(fundamentals_df)):
stock = fundamentals_df["stock"][i]
try:
f_json = requests.get(f'https://eodhd.com/api/fundamentals/{stock}?api_token={api_key}&fmt=json').json()
fundamentals_df['pb'][i] = f_json['Valuation']['PriceBookMRQ']
fundamentals_df['roe'][i] = f_json['Highlights']['ReturnOnEquityTTM']
print(f'{i}. {stock} finished')
except:
print(f'{i}. {stock} ERROR')
fundamentals_df.head()
Начинаем создавать пустой датафрейм fundamentals_df
со столбцами для тикера акции, P/B и ROE.
Перебирая каждый тикер акции, создаем API-запрос для получения фундаментальных данных. Функция requests.get()
получает ответ в формате JSON, из которого извлекаем P/B (PriceBookMRQ
) и ROE (ReturnOnEquityTTM
). Затем эти значения присваиваются соответствующим столбцам в датафрейме.
Блок try-except
обеспечивает предотвращение ошибок во время вызова API, таких как отсутствие данных, проблемы с подключением и т. д.
Итоговый датафрейм выглядит следующим образом:
Шаг 4: Выбор и сортировка акций на основе критериев отбора
Теперь, имея фундаментальные данные по каждой акции, переходим к следующему этапу — сортировке и определению наиболее перспективных акций на основе критериев отбора: P/B ниже 2 и ROE выше 15 %.
# Выбор и сортировка акций на основе критериев отбора
fundamentals_df['pb'], fundamentals_df['roe'] = fundamentals_df['pb'].astype(float), fundamentals_df['roe'].astype(float)
top10_stocks = fundamentals_df[(fundamentals_df.pb < 2) & (fundamentals_df.roe > 0.15)].nlargest(10, 'roe')
top10_stocks
Сначала преобразуем столбцы P/B и ROE в типы данных float, чтобы обеспечить возможность численного сравнения. Затем с помощью критериев отбора отфильтруем в датафрейме акции с P/B ниже 2 и ROE выше 15 %. Функция nlargest()
используется для выбора 10 лучших акций с наибольшим ROE среди тех, которые удовлетворяют этим условиям. Таким образом, получаем список потенциально недооцененных, но прибыльных акций, готовых к дальнейшему анализу или инвестированию.
Согласно критериям отбора, вот 10 лучших акций, а также их P/B и ROE:
Шаг 5: Извлечение исторических данных
Следующий этап после отбора 10 топовых акций, основанном на P/B и ROE, — сбор исторических ценовых данных, извлекаемых с помощью конечной точки исторических данных EODHD, полученных на конец дня. Эти данные будут использованы для бэктеста стратегии.
# Извлечение исторических данных
historical_data = pd.DataFrame(columns = top10_stocks.stock)
for stock in historical_data.columns:
hist_json = requests.get(f'https://eodhd.com/api/eod/{stock}?from=2020-01-01&period=d&api_token={api_key}&fmt=json').json()
hist_df = pd.DataFrame(hist_json)
date, adj_close = hist_df.date, hist_df.adjusted_close
historical_data[stock], historical_data['date'] = adj_close, date
historical_data = historical_data.set_index('date')
historical_data.index = pd.to_datetime(historical_data.index)
historical_data
Начнем с создания пустого датафрейма historical_data
со столбцами, соответствующими каждой из 10 топовых акций.
Для каждой акции создаем запрос к API, чтобы получить ежедневные исторические ценовые данные, начиная с 1 января 2020 года. Данные сохраняются в датафрейме с занесением скорректированных цен закрытия в соответствующий столбец акций.
Фиксируем также даты, которые устанавливаются в качестве индекса датафрейма и преобразуются в формат времени для точного анализа временных рядов. Это подготавливает данные к следующим этапам, когда будет рассчитываться доходность и оцениваться эффективность стратегии.
Итоговый датафрейм выглядит следующим образом:
Шаг 6: Расчет доходности портфеля
Следующим этапом после сбора исторических данных о ценах будет расчет ежедневной доходности портфеля. Эти показатели позволят оценить эффективность стратегии.
# Расчет доходности портфеля
rets_df = historical_data.pct_change().dropna()
rets_df['total_ret'] = rets_df.sum(axis = 1)
rets_df.tail()
Рассчитываем ежедневную доходность для каждой акции в портфеле с помощью функции pct_change()
, которая вычисляет процентное изменение между предыдущим и последующим днем. Функция dropna()
применяется для удаления строк с отсутствующими данными, что обеспечивает точность расчетов.
Чтобы определить общую эффективность портфеля, суммируем ежедневную доходность по всем акциям и сохраняем результат в новом столбце под названием total_ret
. Это даст единый ряд, представляющий ежедневную доходность всего портфеля, который теперь можно использовать для анализа эффективности стратегии.
Вот как выглядит вновь созданный датафрейм rets_df
:
Шаг 7: Визуализация кумулятивной доходности
Рассчитав ежедневную доходность портфеля, переходим к выполнению визуализации кумулятивной доходности. Этот завершающий этап поможет определить общую эффективность стратегии во временной динамике.
# Кумулятивная доходность
rets_df['total_ret'].cumsum().plot()
plt.title('STRATEGY PERFORMANCE')
Используем функцию cumsum()
для расчета кумулятивной суммы показателей ежедневной доходности портфеля. Это дает кумулятивную доходность во временной динамике, отражающую рост (или уменьшение) стоимости портфеля с начала периода бэктеста. Функция plot()
используется для создания очень простого линейного графика, позволяющего быстро оценить эффективность стратегии.
Вывод приведенного выше кода не может не интриговать:
Вот интерпретация графика.
- Первоначальный спад (2020 год): стратегия пережила значительный спад, когда кумулятивная доходность упала ниже -7,5%, что, вероятно, отражает более высокую волатильность рынка в начале 2020 года.
- Быстрое восстановление (2020-2021 гг.): портфель быстро восстановился, перешел в положительную область и продолжал стабильно расти до 2021 года, свидетельствуя о том, что стратегия воспользовалась восстановлением рынка.
- Стабильность и умеренная волатильность (2022-2024 гг.): начиная с 2022 года портфель демонстрирует более стабильные показатели, а кумулятивная доходность колеблется между 5 и 10. Несмотря на замедление роста, стратегия сохраняет положительную доходность, доказывая свою устойчивость.
В целом стратегия демонстрирует быстрое восстановление после первоначальных потерь и сохраняет стабильную положительную доходность на протяжении всего периода.
Заключение
Мы исследовали фундаментальную торговую стратегию, использующую P/B и ROE для выявления недооцененных акций, демонстрирующих высокие финансовые показатели. С помощью Python и надежных конечных точек API, предоставляемых EODHD, мы реализовали и протестировали эту стратегию, выявив ее способность переиграть рынок, то есть получить доход, превышающий среднерыночные показатели.
Результаты оказались убедительными: несмотря на первоначальные проблемы, отраженные в рыночном спаде 2020 года, стратегия продемонстрировала уверенное восстановление и стабильный рост. Совокупная доходность портфеля показала устойчивость, особенно в периоды рыночной нестабильности, что подчеркивает надежность фокусировки на фундаментальных финансовых показателях.
Хотя эта стратегия доказала эффективность, необходимо помнить, что ни один подход не является безошибочным. Постоянный мониторинг, корректировка в зависимости от рыночных условий и надежный план управления рисками — вот что необходимо для поддержания долгосрочного успеха. Тем не менее данная стратегия предлагает инвесторам простой, но эффективный способ выявления потенциально недооцененных, но прибыльных рыночных активов.
Читайте также:
- 10 способов повысить качество Python-кода
- Поврежден жесткий диск? Python спешит на помощь!
- Как собрать данные для DS-проекта с помощью Python: 3 шага
Читайте нас в Telegram, VK и Дзен
Перевод статьи Nikhil Adithyan: A Simple Fundamental Trading Strategy that Beats the Market