Използване на произволна горска регресия за генериране на 1400% възвръщаемост за 15 години

Индексът S&P 500 е един от най-следваните пазарни индекси и често се използва като еталон за общия пазар на акции в САЩ. Въпреки че може да се обмисли инвестиране в пасивен фонд, копиращ S&P500, мнозина ще се насочат към мениджъри на фондове и ще предпочетат да инвестират в активни фондове. За съжаление, все повече и повече проучвания и данни показват, че е прекалено трудно да се „победи пазара“ (надмине бенчмарка). Човек може да отиде „тук“ и „тук“, за да прочете повече по темата и „тази“ отлична книга от Бъртън Г. Малкиел също силно се препоръчва. Едно от заключенията на тези проучвания е, че 91,62 % от активно управляваните капиталови фондове в САЩ са се представили по-слабо от S&P 500 за 15-годишния период, приключващ през декември 2018 г.

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

Бележка от редакторите на Towards 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 repo“.

Човек може да види колко липсващи записи имаме за всяка променлива, използвайки следната команда 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 години ни позволява да получим приблизително цял икономически цикъл за всеки от нашите тренировъчни комплекти. Тестовият набор ще бъде месецът, за който искаме да излъчваме прогнози. Ние ще използваме основна итерация, за да се върнем назад във времето, месец след месец, за да тестваме в продължение на няколко месеца. Инстанцирането на нашите променливи при всяка итерация гарантира, че никаква информация не се разпространява назад във времето (вижте целия код за подробности).

След премахване на ненужни данни за обучение и тестване, функцията инстанцира обект RandomForestRegressor от библиотеката 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 на проекта.

Благодаря ви за отделеното време!