ВВЕДЕНИЕ

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

Но как? Экономические данные часто представляют собой смесь значимых сигналов и случайного шума. Задача состоит в том, чтобы провести различие между ними. Даже экономические прогнозы экономистов имеют смешанный послужной список. В то время как некоторые прогнозы были относительно точными, другие были далеки от истины. Сложность экономических систем затрудняет точное прогнозирование. На экономику влияет множество факторов, в том числе государственная политика, поведение потребителей, технологические изменения, глобальные события и динамика рынка. Учитывая все эти факторы, инвестору кажется невозможным выбрать выигрышную акцию на основе точного прогноза экономического положения.

Поэтому я разработал торговую стратегию с использованием модели LSTM, указав оптимальный вес каждого ETF макроэкономики с учетом последних тенденций экономической статистики.

ДАННЫЕ

  1. 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