Вертолетные деньги закончились, пора узнать, что будет дальше.

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.

Пожалуйста, имейте в виду, что:

  • моделирование основано только на одной переменной: денежно-кредитной политике, поэтому оно не охватывает все объяснения движений рынка и особенно краткосрочную волатильность (геополитика, истечение опционов, сезоны доходов и т. д.),
  • прогнозы модели основаны на гипотетических сценариях будущих решений ФРС,
  • текущая рыночная ситуация не характерна для новейшей истории и периоды с такой высокой инфляцией не представлены в обучающей выборке,
  • это моделирование предназначено только для упражнения и, конечно, не является инвестиционным советом.

Не стесняйтесь критиковать или предлагать улучшения.

Рекомендации

[1] https://www.bloomberg.com/news/articles/2022-03-16/fed-lifts-rates-a-quarter-point-in-opening-bid-to-curb-inflation

[2] https://pypi.org/project/yahooquery/

[3] https://fred.stlouisfed.org/series/WALCL

[4] https://fred.stlouisfed.org/series/INTDSRUSM193N

[5] https://medium.datadriveninvestor.com/should-you-avoid-being-invested-during-earnings-reports-a6cdd8cca0d1

Запланируйте сеанс DDIChat в Data Science / AI / ML / DL:



Подайте заявку на участие в программе DDIChat Expert здесь.
Работайте с DDI: https://datadriveninvestor.com/collaborate
Подпишитесь на DDIntel здесь.