Вертолетные деньги закончились, пора узнать, что будет дальше.
16 марта 2022 г. Федеральная резервная система (ФРС) повысила процентные ставки на 0,25% и сообщила о еще шести повышениях ставок до конца 2022 г. для борьбы с самой высокой инфляцией за четыре десятилетия [1].
Эти повышения процентных ставок — лишь одна сторона ястребиной денежно-кредитной политики ФРС. Другая сторона состоит в том, чтобы сократить свой баланс на $9 трлн, во-первых, остановив покупку ценных бумаг, которая была инициирована для поддержки экономики во время пандемии, а во-вторых, начав продавать часть ее, чтобы уменьшить количество денег в обращении.
В данной статье предпринята попытка спрогнозировать эволюцию индекса S&P500 для гипотетических сценариев эволюции баланса ФРС.
Это рискованное предприятие предназначено только для тренировки и не является инвестиционным советом. В частности, читатель должен иметь в виду, что политика FED — единственная переменная, учитываемая в последующем моделировании.
Эта статья требует некоторых навыков программирования на Python. Если вас интересуют только результаты, перейдите к разделу 6.
1. Набор исторических данных
Первый шаг к началу упражнения состоит в сборе интересующих данных, а именно:
- исторические значения индекса S&P500,
- исторические значения баланса ФРС,
- исторические процентные ставки в США.
Исторические значения S&P500 можно легко загрузить с сайта Yahoo! Финансы с использованием Python и пакета yahooquery [2]:
from yahooquery import Ticker import pandas as pd sp500 = Ticker("^GSPC").history(period='21Y', interval='1d') sp500 = sp500.reset_index() sp500["date"] = pd.to_datetime(sp500["date"]) sp500.set_index("date",inplace=True)
История баланса ФРС и исторические процентные ставки могут быть загружены в файлы CSV с веб-сайта FRED ([3] и [4]). Следующий фрагмент кода считывает данные CSV в кадр данных Pandas (рис. 1):
fed_bs = pd.read_csv(path + "WALCL.csv") rates = pd.read_csv(path + "INTDSRUSM193N.csv") fed_bs.set_index("DATE",inplace=True) rates.set_index("DATE",inplace=True) rates["INTDSRUSM193N"] = rates[rates["INTDSRUSM193N"] != '.'] rates["INTDSRUSM193N"] = rates["INTDSRUSM193N"].astype(float) fed_bs.index = pd.to_datetime(fed_bs.index) rates.index = pd.to_datetime(rates.index) fed = fed_bs.copy() fed['Rates'] = rates["INTDSRUSM193N"] fed = fed.fillna(method="ffill") fed = fed.dropna()
2. Прогнозы денежно-кредитной политики
Поскольку дорожная карта повышения процентных ставок на ближайшие два года кажется относительно ясной, темпы и масштабы сокращения баланса пока неизвестны.
В этом контексте мы затем рассматриваем один сценарий повышения ставок и четыре различных сценария сокращения баланса до 2025 года (рис. 2).
Для этого целевые значения устанавливаются на несколько дат в будущем, а промежуточные значения интерполируются с использованием форвардного заполнения для процентных ставок и квадратичной интерполяции для баланса:
import numpy as np import datetime as dt #extend dates range for date in pd.date_range(start="2022-02-24", end="2027-09-15"): fed.loc[date,:] = np.nan #7 rate hikes of 2022 fed.loc[dt.datetime(2022,3,15),"Rates"] = 0.5 fed.loc[dt.datetime(2022,5,15),"Rates"] = 0.75 fed.loc[dt.datetime(2022,6,15),"Rates"] = 1.0 fed.loc[dt.datetime(2022,7,15),"Rates"] = 1.25 fed.loc[dt.datetime(2022,9,15),"Rates"] = 1.5 fed.loc[dt.datetime(2022,11,15),"Rates"] = 1.75 fed.loc[dt.datetime(2022,12,15),"Rates"] = 2.0 #4 rate hikes of 2023 fed.loc[dt.datetime(2023,3,15),"Rates"] = 2.25 fed.loc[dt.datetime(2023,5,15),"Rates"] = 2.5 fed.loc[dt.datetime(2023,7,15),"Rates"] = 2.75 fed.loc[dt.datetime(2023,9,15),"Rates"] = 3.0 fed.loc[dt.datetime(2027,9,15),"Rates"] = 3.0 #interpolation of interest rates fed["Rates"] = fed["Rates"].fillna(method="ffill") #four balance sheet scenarios fed_forecasts = [("5T",5000000),("7T",7000000),("8T",8000000),("9T",9000000)] #set BS values and interpolate for label,forecast in fed_forecasts: fed["WALCL " + label ] = fed["WALCL"] fed.loc[dt.datetime(2023,12,15),"WALCL " + label ] = forecast fed.loc[dt.datetime(2027,6,15),"WALCL " + label ] = forecast fed.loc[fed.index<=dt.datetime(2023,12,15),"WALCL " + label ]=fed.loc[fed.index<=dt.datetime(2023,12,15),"WALCL " + label ].interpolate(method="quadratic") fed.loc[fed.index>=dt.datetime(2023,12,15),"WALCL " + label ]=fed.loc[fed.index>=dt.datetime(2023,12,15),"WALCL " + label ].interpolate(method="linear") fed.loc[fed.index>=dt.datetime(2023,3,15),"WALCL " + label ]=\ fed.rolling(400,center=True).mean().loc[fed.index>=dt.datetime(2023,3,15),"WALCL " + label ] + \ fed.loc[fed.index<dt.datetime(2023,3,15),"WALCL " + label].iloc[-1] - \ fed.rolling(400,center=True).mean().loc[fed.index==dt.datetime(2023,3,15),"WALCL " + label ].iloc[-1] fed = fed.rename(columns={"WALCL " + label :"BS" + label }) fed["WALCL"] = fed["WALCL"].fillna(0) fed = fed.rename(columns={"WALCL" :"BS" }) fed = fed.dropna()
3. Предварительная обработка данных
На этом этапе у нас есть набор данных с историческим балансом ФРС, историческими целевыми процентными ставками, историческими ценами закрытия S&P500 и прогнозируемыми сценариями для баланса ФРС и целевых процентных ставок (рис. 3).
Поскольку функция баланса ФРС и целевая цена S&P500 представляют собой временные ряды с трендами, первая операция предварительной обработки состоит в преобразовании их в нестационарные временные ряды, чтобы можно было использовать более широкий набор методов моделирования.
Для этого мы различаем временные ряды, вычисляя возврат журнала между двумя временными шагами. Для получения дополнительной информации о возврате журнала прочитайте обязательный раздел следующей статьи [5]:
#add "close" column to the dataset and rolling average it to smooth #small variations data_set = fed.copy() data_set["close"] = sp500["close"] data_set["close"] = data_set["close"].fillna(method="ffill") data_set["close"] = data_set["close"].rolling(15,center=True).mean() data_set = data_set.dropna() #compute log returns data_set_log_m = data_set.resample('1W').mean() for c in data_set_log_m.columns: if "BS" in c: data_set_log_m[c] = np.log(data_set_log_m[c]) - np.log(data_set_log_m[c].shift(1)) data_set_log_m["close"] = np.log(data_set_log_m["close"]) - np.log(data_set_log_m["close"].shift(1))
Вторая операция заключается в добавлении в черты какого-то взгляда на прошлое и предвосхищения будущего. Последнее кажется весьма важным, поскольку мы знаем, что рынок в основном предвидит то, что произойдет в будущем.
Взгляд на прошлое довольно прост: вычисляется скользящее среднее за последние 1, 3, 6, 12 месяцев баланса и процентных ставок.
Предвидение будущего сложнее. В этом упражнении мы считаем ожидание ближайшего будущего идеальным (6 и 12 месяцев), за исключением исключительного увеличения (бутстрапы количественного смягчения) в балансе из-за неожиданных событий: субстандартных кредитов и кризиса Covid (рис. 4).
В действительности прогноз движения баланса ФРС, конечно, не идеален, и это представляет собой первое упрощение для моделирования.
#exclude quantitative easing bootstraps data_set_log_m_no_surprise = data_set_log_m.copy() data_set_log_m_no_surprise.loc[data_set_log_m_no_surprise["BS"]>=0.05]=0 #compute past view and anticipation of the future weeks = 4 for c in data_set_log_m.columns: if "BS" in c: for i in [1,3,6,12]: data_set_log_m[c+"-"+str(i)+"mean"] = data_set_log_m[c].rolling(i*weeks).mean() for i in [6,12]: data_set_log_m[c+"+"+str(i)+"mean"] = data_set_log_m_no_surprise[c].rolling(i*weeks).mean().shift(-(i)*weeks) for i in [1,3,6,12]: data_set_log_m["Rates"+"-"+str(i)+"mean"] = data_set_log_m["Rates"].rolling(i*weeks).mean() for i in [6,12]: data_set_log_m["Rates"+"+"+str(i)+"mean"] = data_set_log_m["Rates"].rolling(i*weeks).mean().shift(-(i)*weeks) #add week of year feature data_set_log_m["week"] = data_set_log_m.index.week #reorganize columns data_set_log_m = data_set_log_m[[c for c in data_set_log_m.columns if c!= "close"] + ["close"]]
4. Модельное обучение
Прежде чем можно будет обучить модель, набор данных делится на три набора:
- обучающая выборка: 80% выборок набора данных до 30 июня 2021 г.,
- проверочный набор: 20% выборок набора данных до 30 июня 2021 г.,
- тестовый набор: образцы набора данных после 30 июня 2021 года.
from sklearn.model_selection import train_test_split split_date = "2021-06-30" training_set = data_set_log_m[data_set_log_m.index<split_date].copy() training_cols = training_set.columns for label,forecast in fed_forecasts: training_cols = [c for c in training_cols if label not in c] training_set = training_set[training_cols] training_set = training_set.dropna() test_sets = {} for l,f in fed_forecasts: test_sets[l] = data_set_log_m[data_set_log_m.index>=split_date].copy() train,val = train_test_split(training_set, test_size=0.2)
Затем функции нормализуются с помощью масштабатора MinMax.
from sklearn.preprocessing import MinMaxScaler scaler = MinMaxScaler() scaled_cols = training_set.columns train[scaled_cols] = scaler.fit_transform(train[scaled_cols]) val[scaled_cols] = scaler.transform(val)
Наконец, модель можно обучить. В этом упражнении мы выбираем регрессор Random Forest для выполнения этой работы.
from sklearn.ensemble import RandomForestRegressor def test_(yhat,X_train, y_train,X_test, y_test): # invert scaling for forecast inv_yhat_full = X_test.copy() inv_yhat_full["yhat"] = yhat inv_yhat_full[inv_yhat_full.columns] = scaler.inverse_transform(inv_yhat_full) inv_yhat = inv_yhat_full.iloc[:,-1] # invert scaling for actual inv_y = X_test.copy() inv_y["y"] = y_test inv_y[inv_y.columns] = scaler.inverse_transform(inv_y) inv_y = inv_y.iloc[:,-1] df_Result = pd.DataFrame() df_result = pd.DataFrame(index=y_test.index) df_result['yhat'] = inv_yhat df_result['y']=inv_y return df_result.sort_index() def train_validate_RF(X_train, y_train,X_test, y_test): model = RandomForestRegressor() model.fit(X_train, y_train) yhat = model.predict(X_test) return (test_(yhat,X_train, y_train,X_test, y_test),model) X_train, y_train = train.iloc[:, :-1], train.iloc[:, -1] X_val, y_val = val.iloc[:, :-1], val.iloc[:, -1] train_result = train_validate_RF(X_train, y_train,X_val, y_val)
Затем можно оценить производительность модели на проверочном наборе и повторять выбор функций и параметры модели, пока не будет найдено удовлетворительное.
Сохраненная модель имеет среднюю абсолютную ошибку, равную 0,0014 (прогнозируемое значение представляет собой недельный доход журнала) и оценку R2, равную 0,89. на проверочном наборе (рис. 5).
Важность этой функции модели представлена на рисунке 6. Главной особенностью является ожидание баланса на ближайшие шесть месяцев.
5. Тестирование модели
Затем производительность окончательной модели оценивается на тестовом наборе, и вычисляются прогнозы для нескольких монетарных политик, определенных в разделе 2.
def test_predict(X_test,model,test_set): yhat_val = model.predict(X_test) inv_yhat_test_full = X_test.copy() inv_yhat_test_full["yhat"] = yhat_val inv_yhat_test_full[inv_yhat_test_full.columns] = scaler.inverse_transform(inv_yhat_test_full) inv_yhat_test = inv_yhat_test_full.iloc[:,-1] df_Result = pd.DataFrame() df_result = pd.DataFrame(index=inv_yhat_test.index) df_result['yhat'] = inv_yhat_test df_result['y']=test_set["close"] return df_result.sort_index() test_and_predict = {} #predict for the 4 scenarios for l,f in fed_forecasts: test = test_sets[l].copy() test_cols = training_cols.copy() for i in range(len(training_cols)): if "BS" in training_cols[i]: test_cols[i] = test_cols[i].replace("BS","BS"+l) test = test[test_cols] test[test_cols] = scaler.transform(test[test_cols]) test = test.dropna() X_test, y_test = test.iloc[:, :-1], test.iloc[:, -1] forecast = test_predict(X_test,model,test_sets[l]) test_and_predict[l] = forecast
В тестовом наборе средняя абсолютная ошибка 0,0025 не так низка, как в обучающем наборе, но, глядя на рис. 7, мы видим, что общая тенденция соблюдается, в то время как краткосрочная волатильность или геополитические события (украинская война) не захвачены (как и ожидалось).
6. Прогнозы сценариев и окончательные результаты
Последним шагом, чтобы получить наши прогнозы для четырех сценариев денежно-кредитной политики, является инвертирование операции возврата журнала:
last_value = data_set.resample("1w").mean([data_set.resample("1w").mean().index < test_sets["5T"].index[0]].iloc[-1,-1] def compute_close(l): test_and_predict[l]["cum_return"] = test_and_predict[l]["yhat"].cumsum() test_and_predict[l]["Price Forecast"] = last_value*np.exp(test_and_predict[l]["cum_return"]) return test_and_predict[l] for l,f in fed_forecasts: test_and_predict[l] = compute_close(l)
Четыре прогноза представлены на рисунке 8.
Как и ожидалось, чем больше сокращается баланс, тем менее оптимистична модель в отношении будущего.
Для сценариев с балансами в 9 и 8 триллионов будущее не так мрачно: доходность составит около 20% к концу 2024 года.
Сценарий 7 трлн кажется более неопределенным с прогнозируемой доходностью не более 10% к концу 2024 года.
Наконец, сценарий на 5 триллионов выглядит как сценарий Мать всех крахов с доходностью -50 % из-за темпов ужесточения, которые, надеюсь, кажутся немного нереалистичными.
Последнее замечание также заключается в том, что рынок, вероятно, ожидает ястребиной политики немного раньше, чем модель, поскольку реальная кривая S&P500 начинает расходиться для прогноза с января 2022 года, в то время как раньше они были почти идеально совмещены. Расхождение также может быть объяснено геополитическими событиями (война на Украине), которые не отражены в модели.
7. Итог
Теперь вы можете (попытаться) предсказать эволюцию индекса S&P500 с помощью баланса ФРС и Python.
Пожалуйста, имейте в виду, что:
- моделирование основано только на одной переменной: денежно-кредитной политике, поэтому оно не охватывает все объяснения движений рынка и особенно краткосрочную волатильность (геополитика, истечение опционов, сезоны доходов и т. д.),
- прогнозы модели основаны на гипотетических сценариях будущих решений ФРС,
- текущая рыночная ситуация не характерна для новейшей истории и периоды с такой высокой инфляцией не представлены в обучающей выборке,
- это моделирование предназначено только для упражнения и, конечно, не является инвестиционным советом.
Не стесняйтесь критиковать или предлагать улучшения.
Рекомендации
[2] https://pypi.org/project/yahooquery/
[3] https://fred.stlouisfed.org/series/WALCL
[4] https://fred.stlouisfed.org/series/INTDSRUSM193N
Запланируйте сеанс DDIChat в Data Science / AI / ML / DL:
Подайте заявку на участие в программе DDIChat Expert здесь.
Работайте с DDI: https://datadriveninvestor.com/collaborate
Подпишитесь на DDIntel здесь.