ВВЕДЕНИЕ
При выборе продуктивного запаса важно учитывать режим рынка, основанный на экономических данных. Принятие торгового решения в условиях экономического статуса имеет большое значение для вашей будущей прибыли.
Но как? Экономические данные часто представляют собой смесь значимых сигналов и случайного шума. Задача состоит в том, чтобы провести различие между ними. Даже экономические прогнозы экономистов имеют смешанный послужной список. В то время как некоторые прогнозы были относительно точными, другие были далеки от истины. Сложность экономических систем затрудняет точное прогнозирование. На экономику влияет множество факторов, в том числе государственная политика, поведение потребителей, технологические изменения, глобальные события и динамика рынка. Учитывая все эти факторы, инвестору кажется невозможным выбрать выигрышную акцию на основе точного прогноза экономического положения.
Поэтому я разработал торговую стратегию с использованием модели LSTM, указав оптимальный вес каждого ETF макроэкономики с учетом последних тенденций экономической статистики.
ДАННЫЕ
- API Фреда
Я искал наиболее часто запрашиваемые данные на веб-сайте FRED и составил словарь данных, собрав их с помощью красивой библиотеки супов. Словарь данных включает информацию о каждой экономической статистике — тикер, название, периодичность, дату начала наблюдения, дату окончания наблюдения и единицы измерения.
Затем я использовал Fred API, чтобы получить экономическую статистику с информацией о тикере и предварительно обработал их, чтобы они подходили для входных данных нашей модели LSTM, таких как удаление данных, недоступных с 2000 года, заполнение NA методом прямого заполнения и указание задержки во избежание предвзятого отношения. Дополнительные сведения о предварительной обработке см. в приведенном ниже коде GetAPI.
2. Данные ETF
Я получил данные ETF, используя API Yahoo Finance. и сопоставил индекс даты между данными FRED и доходностью ETF.
Я выбрал макро-ETF с самым высоким AUM среди ETF с тем же базовым активом. — Доллар (тикер: UUP), облигация (тикер: BITO), золото (тикер: GLD), НЕФТЬ (тикер: USO), рынок (тикер: SPY), фактор размера (тикер: SIZE), фактор стоимости (тикер: VLUE ), фактор качества (тикер: QUAL), низкая волатильность (тикер: USMV), фактор импульса (тикер: MTUM), высокие дивиденды (тикер: VIG)
ОБУЧЕНИЕ МОДЕЛИ
Во-первых, я создал набор данных временных рядов с скользящим окном, который является входными данными, которые будут переданы в модель. Одной единицей входных данных будет 60-дневная экономическая статистика и последующие 30-дневные доходы ETF без перекрывающихся периодов между двумя данными. Затем я сгенерировал несколько единиц входных данных, передвигая окно на 1 день. Обучение, проверка и период испытаний этой модели следующие.
The train period is 2013.10.14 ~ 2020.03.27 The validation period is 2020-03-30 ~ 2020-12-14 The test period is 2020-12-15 ~ 2022-10-28
Результатом модели будет оптимальный вес, при котором каждый макроактив будет работать лучше всего, максимизируя коэффициент Шарпа результирующего портфеля. Для этого я настроил целевую функцию вместо MSE, которая встроена в TensorFlow в качестве функции потерь по умолчанию.
Я разработал модель с двумя многослойными слоями LSTM и установил коэффициент отсева равным 0,5, чтобы решить проблему переобучения. Функция активации выходного слоя — это функция softmax, учитывающая ограничения коротких продаж.
ПРОВЕРКА
Это, безусловно, портфолио только для длинных позиций, так как я назначил функцию softmax выходному слою. Период ребалансировки составляет один месяц. На графике ниже показан оптимальный вес каждого ETF в период тестирования.
Портфель, инвестирующий в каждый ETF по весу модели, имеет коэффициент Шарпа 0,65, тогда как у рыночного портфеля 0,19. Модельный ряд опередил рынок более чем в 3 раза. Интересно, что портфель, инвестирующий в ETF, который имеет самый высокий вес модели, показывает замечательный коэффициент Шарпа, 2,17. Это может быть связано с тем, что российско-украинская война привела к высокой динамике цен на нефть в тестовый период.
ЗАКЛЮЧЕНИЕ И ВЫВОДЫ
В этом исследовании я решил сложную задачу выбора акций с использованием экономических данных, разработав торговую стратегию на основе модели LSTM для оптимизации макроэкономических ETF и максимизации коэффициента Шарпа. Мой подход заключался в том, чтобы зафиксировать нелинейную динамику экономической статистики и применить ее к выбору акций. Модель превзошла рынок с коэффициентом Шарпа 0,65, демонстрируя потенциал использования экономической информации из публично объявленной экономической статистики.
ПОЛНЫЙ КОД
from tqdm import tqdm import pandas as pd import numpy as np import matplotlib.pyplot as plt from fredapi import Fred from dotenv import load_dotenv import datetime import os import yfinance as yf import warnings warnings.filterwarnings('ignore') from sklearn.preprocessing import StandardScaler from sklearn.utils import shuffle from keras.models import load_model import tensorflow as tf import tensorflow_probability as tfp import keras_tuner as kt from sklearn.metrics import make_scorer from scipy.stats import uniform from sklearn.model_selection import RandomizedSearchCV from tensorflow.keras.callbacks import Callback, ReduceLROnPlateau, ModelCheckpoint, EarlyStopping class GetAPI: def get_data_dic(self): data_dic = pd.read_csv('data_dic_df_ver2.csv') return data_dic def get_fred_df(self): # get fred_api key load_dotenv() fred_key = os.getenv("API_KEY") fred = Fred(api_key=f'{fred_key}') # get data dictionary df data_dic = self.get_data_dic() ticker = data_dic.ticker # collect the macro economic statistics included in the tickers of fred_df dfs = [] for i in range(len(ticker)): dfs.append(fred.get_series(ticker[i])) print(f'{i}: {data_dic.title[i]} has been downloaded ') today = datetime.date.today() df_all = pd.concat(dfs, axis=1) df_all = df_all['1999':today.isoformat()] df_all.columns = ticker.values df = df_all.fillna(method='ffill')['2000':] return df def fred_preprocessing(self, latency_d=1, latency_m=1, latency_q=1): data_dic = self.get_data_dic() df_fred = self.get_fred_df() ticker = data_dic.ticker # collect the macro economic statistics included in the tickers of fred_df df_f = df_fred.loc[:, ticker] # sorting the statistics where the frequency is daily and give them latency df_d = df_f.loc[:, data_dic.ticker[data_dic.frequency == 'D'].to_list()] df_d_shift = df_d.shift(latency_d) # sorting the statistics where the frequency is monthly and give them latency ticker_m = data_dic.ticker[data_dic.frequency == 'M'].to_list() df_m = df_f.resample('M').last()[ticker_m] df_m_shift = df_m.shift(latency_m) df_m_shift_idx = df_m_shift.index.to_list() df_m_shift_idx[-1]= df_f.index[-1] df_m_shift.index = df_m_shift_idx # sorting the statistics where the frequency is quarterly and give them latency ticker_q = data_dic.ticker[data_dic.frequency == 'Q'].to_list() df_q = df_f.resample('Q').last()[ticker_q] df_q_shift = df_q.shift(latency_q) df_q_shift_idx = df_q_shift.index.to_list() df_q_shift_idx[-1]= df_f.index[-1] df_q_shift.index = df_q_shift_idx # merge df_ipt = df_d_shift.join(df_m_shift, how='outer') df_ipt = df_ipt.join(df_q_shift, how='outer') df_ipt = df_ipt.fillna(method='ffill').dropna() return df_ipt def get_ETF_return(self, start_date='2000-09-30', end_date=datetime.date.today()): dollar = yf.download('UUP', start_date, end_date)['Adj Close'] bond = yf.download('BND', start_date, end_date)['Adj Close'] gold = yf.download('GLD', start_date, end_date)['Adj Close'] oil = yf.download('USO', start_date, end_date)['Adj Close'] spy = yf.download('SPY', start_date, end_date)['Adj Close'] size = yf.download('SIZE', start_date, end_date)['Adj Close'] value = yf.download('VLUE', start_date, end_date)['Adj Close'] quality = yf.download('QUAL', start_date, end_date)['Adj Close'] low_vol = yf.download('USMV', start_date, end_date)['Adj Close'] momentum = yf.download('MTUM', start_date, end_date)['Adj Close'] high_div = yf.download('VIG', start_date, end_date)['Adj Close'] macro_factor_df = pd.DataFrame(index=dollar.index) for x in ['dollar', 'bond', 'gold', 'oil', 'spy', 'size', 'value','quality','low_vol','momentum','high_div']: locals()[f'{x}_ret'] = locals()[f'{x}'].pct_change() macro_factor_df = pd.merge(macro_factor_df, locals()[f'{x}_ret'], left_index=True, right_index=True, how='outer') macro_factor_df.columns = ['dollar', 'bond', 'gold', 'oil', 'spy', 'size', 'value','quality','low_vol','momentum','high_div'] macro_factor_df = macro_factor_df.dropna() return macro_factor_df def match_index(self, fred, etf): df_macro = fred.loc[etf.index] etf_ret = etf return df_macro, etf_ret class DynamicMacro(GetAPI): def __init__(self, date, df_macro, etf_ret) -> None: self.date = date self.df_macro = df_macro self.etf_ret = etf_ret def get_data_scaled(self, df): # get the fred dataset used to train the model df_macro_train = self.df_macro[:'2023-03-28'] # get sclaer scaler = StandardScaler() scaler.fit(df_macro_train) df_scaled = scaler.transform(df) return df_scaled def get_input_data(self): date = self.date historical_fred_arr = np.array(self.df_macro.loc[:date][-60:]) historical_fred_arr_scaled = self.get_data_scaled(df=historical_fred_arr) historical_fred_arr_scaled_reshape = historical_fred_arr_scaled.reshape(1, historical_fred_arr_scaled.shape[0], historical_fred_arr_scaled.shape[1]) return historical_fred_arr_scaled_reshape def run_model(self): gamma = 1.0 lamda = 0.0 # factor_rtn shape 6*20 def markowitz_objective(y_true, y_pred): W = tf.expand_dims(y_pred, axis=1) #TensorShape([6, 1]) factor_rtn = y_true R = tf.expand_dims(tf.reduce_mean(factor_rtn, axis=1), axis=2) #TensorShape([100, 6, 1]) C = tfp.stats.covariance(factor_rtn, sample_axis=1) rtn = tf.matmul(W,R) cov = tf.matmul(W, tf.matmul(C, tf.transpose(W, perm=[0,2,1]))) * gamma reg = tf.reduce_sum(tf.square(W), axis=-1) * lamda obj = (rtn / tf.sqrt(cov)) - reg return -tf.reduce_mean(obj, axis=0) model = load_model('Dmodel.h5', custom_objects={'markowitz_objective':markowitz_objective}) return model def get_model_output(self): model = self.run_model() input_data = self.get_input_data() y_pred = model.predict(input_data) wet_df = pd.DataFrame(y_pred, index=[self.date], columns=self.etf_ret.columns) return wet_df