Использование случайной лесной регрессии для получения 1400% прибыли за 15 лет

Индекс S&P 500 является одним из наиболее популярных рыночных индексов и часто используется в качестве ориентира для общего рынка акций США. Хотя можно было бы рассмотреть возможность инвестирования в пассивный фонд, имитирующий S & P500, многие обратятся к управляющим фондами и предпочтут инвестировать в активные фонды. К сожалению, все больше и больше исследований и данных показывают, что превзойти рынок (превзойти эталон) чрезвычайно сложно. Можно перейти сюда и сюда, чтобы узнать больше по предмету, также настоятельно рекомендуется эта отличная книга Бертона Г. Малкила. Один из выводов этих исследований заключается в том, что 91,62% активно управляемых фондов акций в США отставали от индекса S&P 500 за 15-летний период, закончившийся в декабре 2018 года.

В этой статье представлен системный подход, основанный на машинном обучении, который может значительно превзойти S&P 500 за тот же период.

Примечание редакторам Data Science. Хотя мы разрешаем независимым авторам публиковать статьи в соответствии с нашими правилами и рекомендациями, мы не поддерживаем вклад каждого автора. Не стоит полагаться на работы автора без консультации с профессионалами. См. Подробности в наших Условиях для читателей.

Цели и методология

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

  1. Сбор и подготовка данных
  2. Очистка данных
  3. Выбор функции
  4. Обучение и тестирование на истории случайного регрессора леса

* Обратите внимание, что наборы данных, используемые в этом проекте, получены из частных источников (Standard & Poor’s и Bloomberg) и поэтому не могут быть опубликованы. Поэтому мы пропустим шаг 1 в этой статье. Те, у кого есть доступ к наборам данных через необходимые подписки, могут вместо этого обратиться к полной записной книжке, размещенной в следующем проекте Github: SP1500StockPicker.

Метод обучения ансамблю случайных лесов

Метод случайного леса основан на множественных деревьях решений. Единое дерево решений строится нисходящим методом, когда набор данных следует серии логических разделений на основе значений переменных, что приводит к наиболее точному разделению на каждом узле. Каждое уникальное дерево решений обучается на разных выборках обучающего набора с заменой (то есть выборка заменяется на более крупные наборы данных). Регрессия случайного леса усредняет вместе несколько деревьев решений для получения прогноза. Эта статья не будет углубляться в детали этой техники, но больше информации о методе случайного леса можно найти в этих замечательных статьях Prashant Gupta и George Seif.

Рабочая платформа

Следующий код был написан на Python в Jupyter Notebook. Среда JupyterHub использовалась для размещения кода на вычислительном сервере. Большая часть манипуляций с данными производилась с использованием библиотек Pandas и NumPy. Алгоритм машинного обучения основан на обширной библиотеке Scikit-learn. Для распараллеливания некоторых участков кода использовался пакет multiprocessing от Python. Наконец, процесс выбора функций был основан на пакете feature-selector.

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

Очистка данных

Начальный набор данных состоит из 86 переменных. Краткий обзор первых 5 записей представлен на следующем рисунке:

Некоторые из наиболее важных переменных показаны в таблице ниже. Все остальные переменные определены в файле variables.txt в репозитории Github.

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

dataset.isna().sum()

trt1m                5141
exchg                   0
tpci                    0
adate                 441
qdate                   0
public_date             0
CAPEI                2332
bm                   4764
evm                  1942
pe_op_basic          9984
pe_op_dil           74009
pe_exi              10594
pe_inc              10261
ps                    475

roe                  5294
roce                 3336
                    ...  
dltt_be              5698
debt_assets           532
debt_capital         5328
de_ratio              535
intcov              63530
intcov_ratio        63542
cash_ratio          56652
quick_ratio         56634
curr_ratio          56635

sprtrn                  0
win                     0
decision_date           0
SP1500                  0
Length: 86, dtype: int64

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

  1. Строки, в которых отсутствует ежемесячная доходность акций, были удалены, поскольку мы не можем торговать недостающей доходностью.
  2. Переменные, в которых более 50% строк были пустыми, были удалены.
  3. Для отсутствующих финансовых коэффициентов мы использовали пакет SimpleImputer из Scikit-learn, чтобы заменить отсутствующие значения средним значением переменной для всей базы данных. Такой подход крайне ленив и добавит в данные ошибок моделирования. Более подходящим подходом было бы использование среднего значения за период n месяцев для каждой конкретной акции. Тем не менее, был использован ленивый подход, и следующий код суммирует 3 шага, перечисленных здесь.
#Dropping rows with now "trt1m" values:
database = database[np.isfinite(database['trt1m'])]
# Dropping columns with more than 50% of the rows missing:
database = database.loc[:, database.isnull().mean() < .5]
#Looping through the variables to replace NaNs with the average:
imp=SimpleImputer(missing_values=np.nan, strategy="mean" )
impute_columns=database.columns[database.isna().any()].tolist()
for i in impute_columns:
    database[i] = imp.fit_transform(database[[i]])

Для завершения процесса очистки были предприняты дополнительные шаги, которые можно найти в полной записной книжке, вставленной в конце этой статьи или размещенной на моем Github.

Выбор функции

Использовался пакет feature_selector от WillKoehrsen. Его статью можно прочитать здесь, чтобы ознакомиться с его функциями.

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

Выявить сильно коррелированные функции и распечатать корреляционную матрицу очень просто с помощью пакета feature_selector:

fs.identify_collinear(correlation_threshold=0.975)
fs.plot_collinear(plot_all=True)

Из приведенного выше графика мы можем видеть, что некоторые функции имеют высокую корреляцию (больше красного цвета) и 5 ​​функций с корреляцией выше 0,975 были идентифицированы функцией.

Затем мы хотим идентифицировать функции с нулевой важностью. Эти функции не будут использоваться для разделения каких-либо узлов дерева решений и, следовательно, могут быть удалены для сокращения времени обучения. Пакет feature_selector реализует машину повышения градиента из библиотеки LightGBM. Функция усреднит важность функций для 10 обучающих прогонов, чтобы уменьшить дисперсию.

fs.identify_zero_importance (task = 'regression', eval_metric = 'auc', n_iterations = 10, early_stopping = True)
1 features with zero importance after one-hot encoding.

Функции низкой важности также можно определить с помощью пакета feature_selector. В этом методе используются результаты выбора функций с нулевой важностью для определения функций, которые не требуются для получения 99% общей важности.

fs.identify_low_importance(cumulative_importance = 0.99)
69 features required for cumulative importance of 0.99 after one hot encoding.
5 features do not contribute to cumulative importance of 0.99.

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

all_to_remove = fs.check_removal()
database = database.drop(columns = all_to_remove)
Total of 9 features identified for removal:
['ptpm', 'tpci', 'debt_invcap', 'debt_ebitda', 'opmad', 'aftret_equity', 'dltt_be', 'cfm', 'capital_ratio']

Алгоритм машинного обучения

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

Первый раздел функции берет дату решения и определяет предыдущий месяц. Затем он создает обучающий набор от предыдущего месяца до 7 лет назад. Эта продолжительность была выбрана потому, что известно, что средняя продолжительность последних 33 экономических циклов составляла 56 месяцев. Текущий цикл расширения длится 10 лет. 7 лет позволяют нам получить примерно полный экономический цикл для каждого нашего тренировочного набора. Набор тестов будет месяцем, на который мы хотим выпустить прогнозы. Мы будем использовать базовую итерацию, чтобы вернуться во времени, месяц за месяцем, чтобы тестировать на истории в течение нескольких месяцев. Создание экземпляров наших переменных на каждой итерации гарантирует, что никакая информация не будет передана назад во времени (подробности см. В полном коде).

После удаления ненужных данных для обучения и тестирования функция создает экземпляр объекта aRandomForestRegressor из библиотеки sklearn и инициализирует его, используя количество деревьев (n_estimators) и доступных процессов (n_jobs), предоставленных в качестве входных параметров. . Затем функция обучает алгоритм на обучающем наборе и выдает прогнозы для каждой акции SP1500 в течение месяца тестирования.

Последний раздел функции возвращает различные значения функций для дальнейшего анализа.

Например, можно протестировать функцию за декабрь 2018 года:

results,feature_imp,importances,train_feature_list=rfstockpicker_backtest(database,("2018-12-01"),7,1,100,24)

Этот вызов функции «rfstockpicker_backtest» вернет следующую информацию:

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

portfolio=results[0].nlargest(10, 'predictions')
portfolio

Усреднение этих доходностей с равными весами (арифметическое усреднение) дает доходность 2,09% за декабрь 2018 г. по сравнению с SP500, который показал доходность 1,79% за тот же период.

Модель можно улучшить, посмотрев на важность функций:

Хотя на этом рисунке многие функции сжаты по оси x, все же можно увидеть, что некоторые из них имеют очень низкую важность. Функции с важностью ниже 0,02 можно удалить с помощью следующей команды:

df_importance = pd.DataFrame(feature_imp, columns = ['Feature', 'Importance']) 
feature_to_drop=df_importance.Feature[df_importance['Importance']<0.02]
new_db=database.drop(columns=feature_to_drop)

Эта новая база данных позволяет нам увеличить ежемесячную доходность за декабрь 2018 г., вернув 3,57%, что на + 1,48%.

Теперь давайте протестируем нашу модель на истории за более длительный период в 15 лет, с декабря 2003 года по декабрь 2018 года:

results,feature_imp,importances,train_feature_list=rfstockpicker_backtest(new_db,("2018-12-01"),7,(12*15),100,24)

Основной цикл итераций начнется в декабре 2018 года и вернется на 1 месяц в течение 12 * 15 или 180 месяцев, чтобы охватить весь период тестирования на истории. За 180-месячный тестовый период комбинированная доходность +1421% была получена для нашей модели по сравнению с +261% совокупной доходности для актуальный индекс SP500. Таким образом, модель принесла + 1160% сверхдохода за 15-летний период.

Заключительные замечания

Для упрощения проекта были выдвинуты три основные гипотезы:

  1. Все акции, купленные в предыдущем месяце, были проданы, и за каждый месяц тестирования покупалось 10 новых акций. Следовательно, возможно, что акция могла быть продана в конце месяца, а затем куплена снова на следующий день.
  2. Затраты по сделке не учитывались, что могло бы повлиять на совокупную доходность модели. За каждый месяц тестирования было куплено 10 акций и продано 10 акций.
  3. Дивиденды не учитывались, что также повлияло бы на совокупную доходность модели.

В код можно также внести множество улучшений. Дополнительные методы машинного обучения и нейронные сети будут рассмотрены в следующих статьях. Будьте на связи!

Если у вас есть какие-либо комментарии или вопросы, не стесняйтесь писать их ниже! Я постараюсь ответить на них. Код статьи доступен в Jupyter Notebook из Github Repo проекта.

Спасибо за уделенное время!