В конце моей последней статьи я упомянул о своем страстном желании «увидеть» фондовые рынки через призму машинного обучения. В частности, я хотел найти способ извлечь уроки из исторических данных для прогнозирования будущих изменений. Прочитав практически все доступные публичные статьи на стыке машинного обучения и финансовых рынков, я наткнулся на книгу Лопеса Де Прадо «Достижения в области финансового машинного обучения». Я был впечатлен научной ортодоксальностью автора, что, безусловно, помогло мне сократить время моего обучения. Однако практически все самые изощренные методики, тщательно разработанные в книге, в моем случае не принесли результатов. Я должен был выяснить свой собственный набор функций.
В конце концов, я разработал свой собственный набор функций, основанный на моем собственном понимании рынков, чтобы правильно прогнозировать доходность акций на один день вперед. Я также разработал несколько моделей для больших промежутков времени. Последние не входят в предмет рассмотрения данной статьи.
Я бы сказал, что во время этого пути главной задачей было преодолеть высокое отношение шума к сигналу. Эффективные/умные участники рынка (умные деньги) используют различные уловки, чтобы замаскировать свои интервенции на рынках. Они постоянно производят фальшивые триггеры быков/медведей, чтобы ввести в заблуждение остальную часть рынка. Создайте неопределенность и используйте эту неопределенность для получения значительной прибыли с течением времени.
Разделение шума и сигнала — это новый рубеж, и машинное обучение оказалось способным выполнять эту работу при разумном применении.
Мотивация:
Работа с финансовыми рынками становится все более и более сложной из-за увеличения количества котируемых финансовых активов и принятия сложных стратегий. Становится невозможным отслеживать большой выбор акций с помощью ручных методов, если вы не готовы посвятить этому большую часть своего дня.
Классические алгоритмы предлагают способ преодолеть эти проблемы. Они переводят собственное понимание рынка трейдером в автоматизированную торговую систему. Он может отслеживать колебания цен и выполнять заказы при обнаружении сигналов. В большинстве случаев они многословны, потому что мы должны включить все сценарии.
Большой вопрос.Может ли машинное обучение превзойти способность трейдера учиться на рынках и обнаруживать поведение, неразличимое человеческим мозгом?
Мы дадим четкий ответ на этот вопрос в этой статье.
Сбор данных:
Нам необходимо собрать точный набор исторических данных (открытие, максимум, минимум, закрытие и объем) для каждой части исследования акций. Каждая акция демонстрирует разное поведение. Таким образом, у каждой акции будет своя модель машинного обучения.
Вы можете получить бесплатно исторические данные из разных источников:
1- Вы можете найти в среде различные статьи о библиотеках API и Python, которые это делают. Данные могут быть иногда беспорядочными. Вам придется выполнять различные операции, чтобы очистить его.
2- Вы также можете собирать данные с веб-сайтов, поддерживающих парсинг. Вы можете найти много статей, объясняющих, как это сделать.
Как только мы получим наши данные для каждой акции (временной ряд), нам нужно сделать их стационарными.
Стационарность временного ряда:
В наиболее интуитивном смысле стационарность означает, что статистические свойства процесса, генерирующего временной ряд, не меняются во времени. Это не означает, что серия не меняется со временем, просто способ ее изменения сам по себе не меняется со временем.
Если временной ряд акций нестационарен, модели машинного обучения со временем не смогут обучаться. Статистические свойства акций будут меняться со временем, так что учиться будет нечему.
Определение, которое мы сохраняем для стационарности, - это то, которое поддерживает постоянное среднее значение и дисперсию во времени. Чтобы сделать наши данные стационарными, нам нужно выполнить дробное дифференцирование:
import numpy as np Importinguseful tools import pandas as pd import pywt import matplotlib.pyplot as plt import statsmodels.tsa.stattools as ts from sklearn.preprocessing import normalize from statsmodels.tsa.stattools import adfuller import seaborn as sns df = pd.read_csv(‘C:/Users/Desktop/yourstock.csv’, parse_dates=[‘Date’], index_col=[“Date”]) # opening historical data using panda df_log=pd.DataFrame() # creating the log table df_fd=pd.DataFrame() # creating the fractional differentiation table
############# Применение логарифмических преобразований для удаления экспоненциальной дисперсии ##########
df_log[‘Open’]= np.log(df.Open) df_log[‘High’]= np.log(df.High) df_log[‘Low’]= np.log(df.Low) df_log[‘Close’]= np.log(df.Close) df_fd=df # reinitializing the table to df
############# генерация дробно дифференцированного временного ряда #####################
df_fd[[‘Open_dff’]]=fracDiff_FFD(df_log[[‘Open’]].resample(‘1D’).last().dropna(),d=0.21728515625,thres=0.00005) df_fd[[‘High_dff’]]=fracDiff_FFD(df_log[[‘High’]].resample(‘1D’).last().dropna(),d=0.22807397643208566 ,thres=0.00005) df_fd[[‘Low_dff’]]=fracDiff_FFD(df_log[[‘Low’]].resample(‘1D’).last().dropna(),d=0.2265625,thres=0.00005) df_fd[[‘Close_dff’]]=fracDiff_FFD(df_log[[‘Close’]].resample(‘1D’).last().dropna(),d=0.2265625,thres=0.00005)
d - константа дробного дифференцирования, которая делает временной ряд стационарным, не удаляя слишком много памяти для прогнозов. d генерируется следующим кодом:
def GetMin(Stock,thres,d_0,d_1,tick=’Close’): from statsmodels.tsa.stattools import adfuller path,instName=’C:/Users/Desktop/’, Stock_name # you need to write your path out0=pd.DataFrame(columns=[‘adfStat’,’pVal’,’lags’,’nObs’,’95% conf’,’corr’]) df0=pd.read_csv(path+instName+’.csv’,index_col=[‘Date’],parse_dates=[‘Date’]) df1=np.log(df0[[tick]]).resample(‘1D’).last() df1=df1.dropna() d0=d_0 d1=d_1 count=10 delta =10 while delta > 0.01: if count==10 or count==0: df2=fracDiff_FFD(df1,d0,thres) df2=adfuller(df2[tick+’_fd’],maxlag=1,regression=’c’,autolag=None) adS0= df2[0] if count==10 or count==1: df2=fracDiff_FFD(df1,d1,thres) df2=adfuller(df2[tick+’_fd’],maxlag=1,regression=’c’,autolag=None) adS1= df2[0] d2=(d0+d1)/2 df2=fracDiff_FFD(df1,(d0+d1)/2,thres) corr=np.corrcoef(df1.loc[df2.index,tick],df2[tick+’_fd’])[0,1] df2=adfuller(df2[tick+’_fd’],maxlag=1,regression=’c’,autolag=None) adS3= df2[0] if adS3>df2[4][‘5%’]: d0 = (d0+d1)/2 count=0 else: d1= (d0+d1)/2 count=1 delta = abs(adS3-df2[4][‘5%’]) return d2
Вы можете найти код fracDiff_FFD и getWeights в упомянутой выше книге.
ОЧЕНЬ ОЧЕНЬ ВАЖНО!: Если вы имеете дело с большими временными рядами за очень длительный период времени, с крутыми склонами в начале и конце, дробное дифференцирование не сделает ваши данные стационарными. Модель будет различать две отдельные популяции и в основном учиться отдавать предпочтение одному периоду по сравнению с другим. Выбор периода имеет решающее значение для машинного обучения. Мы не можем просто слепо применять теорию и надеяться на лучшее.
Создание признаков:
Теперь мы можем начать использовать наш временной ряд для извлечения функций, которые питают модель. Для целей этой статьи я представлю две функции:
1- Пружинный эффект: это дельта между ценой акции и ее вейвлет-преобразованием (может интерпретироваться как аппроксимация отклонения цены от среднего значения). Для тех, кто не знаком с вейвлет-преобразованиями, они более сложны, чем преобразования Фурье, в том смысле, что они сохраняют время. Мы можем извлечь компоненты вейвлета, удалить те, которые связаны с шумом, затем рекомбинировать оставшиеся компоненты, чтобы восстановить исходный временной ряд (сигнал) без шума. Из-за высокого отношения шума к сигналу рыночных временных рядов удаление одного или двух компонентов удаляет большую часть прогностической памяти.
Для выполнения этого преобразования вам понадобится следующий код:
df_fd[‘spring_effect’]=np.nan df_fd[‘spring_effect_Open’]=np.nan df_fd[‘spring_effect_High’]=np.nan df_fd[‘spring_effect_Low’]=np.nan df_fd.loc[264:,[‘spring_effect’]] = [[(df_fd[‘Close_dff’][i]-waveletfilter(df_fd[‘Close_dff’][i-264+1:i+1])[264–1])/ waveletfilter(df_fd[‘Close_dff’][i+1–264:i+1])[264–1]] for i in range(264,len(df_fd))] df_fd.loc[264:,[‘spring_effect_Open’]] = [[(df_fd[‘Open_dff’][i]-waveletfilter(df_fd[‘Open_dff’][i-264+1:i+1])[264–1])/ waveletfilter(df_fd[‘Open_dff’][i-264+1:i+1])[264–1]] for i in range(264,len(df_fd))] df_fd.loc[264:,[‘spring_effect_Low’]] = [[(df_fd[‘Low_dff’][i]-waveletfilter(df_fd[‘Low_dff’][i-264+1:i+1])[264–1])/ waveletfilter(df_fd[‘Low_dff’][i-264+1:i+1])[264–1]] for i in range(264,len(df_fd))] df_fd.loc[264:,[‘spring_effect_High’]] = [[(df_fd[‘High_dff’][i]-waveletfilter(df_fd[‘High_dff’][i-264+1:i+1])[264–1])/ waveletfilter(df_fd[‘High_dff’][i-264+1:i+1])[264–1]] for i in range(264,len(df_fd))] Where waveletfilter is given by the following code: def waveletfilter(signal, thresh = 0.2, wavelet=”sym5", mode=’symmetric’): # in addition to sync5, there are many other wavelets you can choose from thresh = thresh*np.nanmax(signal) coeff = pywt.wavedec(signal, wavelet, mode) reconstructed_signal = pywt.waverec(coeff[:-4]+ [None] * 4, wavelet, mode) return reconstructed_signal
2- Вторая особенность это улыбка (волатильность). Очень легко кодировать в разные периоды времени:
df_fd[‘Smile_Close_2’]=df_fd[‘Close_dff’].rolling(2).std().diff(periods=1) df_fd[‘Smile_Close_3’]=df_fd[‘Close_dff’].rolling(3).std().diff(periods=1) df_fd[‘Smile_Close_4’]=df_fd[‘Close_dff’].rolling(4).std().diff(periods=1) df_fd[‘Smile_Close_5’]=df_fd[‘Close_dff’].rolling(5).std().diff(periods=1) df_fd[‘Smile_Close_6’]=df_fd[‘Close_dff’].rolling(6).std().diff(periods=1) df_fd[‘Smile_Close_7’]=df_fd[‘Close_dff’].rolling(7).std().diff(periods=1) df_fd[‘Smile_Close_8’]=df_fd[‘Close_dff’].rolling(8).std().diff(periods=1)
Обучение модели:
Чтобы учиться на временных рядах, глубокое обучение бесполезно, независимо от того, сколько слоев вы добавляете. Наиболее эффективными моделями являются классификаторы. Существуют классификаторы бэггинга и классификаторы случайного леса (RF). У них обоих есть плюсы и минусы. Например, RF требует меньше вычислительных ресурсов, чем бэггинг. Вы можете попробовать оба из них и посмотреть, какой из них решает ваши технические проблемы и дает наилучшие результаты.
В этом примере мы передаем классификатору наши функции, чтобы узнать, при каких условиях рынок генерирует ежедневную доходность выше 1% (отмечено 1), а когда нет (отмечено -1).
Импорт полезных инструментов
from sklearn.utils import resample from joblib import dump, load from imblearn.ensemble import BalancedBaggingClassifier,BalancedRandomForestClassifier from sklearn.multiclass import OneVsRestClassifier ########### labelling daily returns above and below 1%############### df_fd[‘bin’]= np.nan df_fd.loc[:-1,[‘bin’]] = [[1] if (df_fd[‘Close_dff’][i+1]-df_fd[‘Close_dff’][i])>=0.01 else [-1] for i in range(0,len(df_fd)-1)] ########### Training the model ############### df1_100=df_fd[[‘spring_effect’,’spring_effect_Open’,’spring_effect_High’, ‘spring_effect_Low’, ‘Smile_Close_2’,’Smile_Close_3',’Smile_Close_4'’Smile_Close_5', ‘Smile_Close_6’, ‘Smile_Close_7’, ‘Smile_Close_8’, ‘bin’]][:-48] df1_100_minority= df1_100[df1_100[‘bin’]==1] # we separate the two populations to match the size df1_100_majority= df1_100[df1_100[‘bin’]==-1] df1_100_majority_ = resample(df1_100_majority, replace=False, # sample with replacement n_samples=int(df1_100.bin.value_counts()[1]), # to match minority class. random_state=None) df1_100= pd.concat([df1_100_minority,df1_100_majority_]).sort_index() df1_100=df1_100.dropna() y_col = ‘bin’ x_cols = list(df1_100.columns.values) x_cols.remove(y_col) X_train = df1_100[x_cols].values Y_train = df1_100[y_col].values cls = BalancedRandomForestClassifier(n_estimators=1000,class_weight=’balanced_subsample’, criterion=’entropy’) cls.fit(X_train, Y_train) # here you train your model dump(cls, ‘models/model_name.joblib’) # here you save your model for future use.
Теперь вы можете использовать сгенерированную модель для прогнозирования невидимых данных.
Прогнозирование:
Теперь мы можем загрузить модели и передать им невидимые данные. Это можно выполнить следующим образом:
from joblib import load from imblearn.ensemble import BalancedRandomForestClassifier cls_rn_for = load(‘models/model_name.joblib’) x_cols = list(df_forecast.columns.values) # df_forecast being the unseen data features. we can generate them using the above methods X_forecast = df_forecast[x_cols].values probas = cls_rn_for.predict_proba(X_forecast) # using predictive power of the model to generate probabilities df2[‘pro’]= probas[:,1] # add probability column to the initial table To add more precision, we could generate multiple models for each stock, use them to generate multiple predictions then average the results. df2[‘avg’] = df2.filter(like=’pro_’).mean(axis=1) df2 = df2[[‘vol_1’,’Close’,’spring_effect’,’rn’,’avg’]] print(df2)
В следующей таблице показаны результаты:
Когда мы смотрим на результаты, мы можем подумать, что прогнозы не точны. Но когда мы внимательно изучаем цифры, мы видим, что результаты довольно хороши, когда колебания рынка очень высоки (более -4%). Например, 25 сентября рынок просел почти на 8%, модель показала 83% вероятность того, что следующая дневная доходность будет выше 1%. Что и произошло, на следующий день рынок взлетел более чем на 4%. Я пробовал этот подход с разными классами активов. Каждый раз я получал стабильные результаты.
Моя цель не в том, чтобы поделиться самой выигрышной стратегией, а в том, чтобы дать тем из вас, кому сложно реализовать свои собственные модели машинного обучения, современный метод для эффективной реализации своих стратегий.
Теперь, когда у вас есть метод создания классификаторов для прогнозирования будущих изменений, вы можете использовать свои собственные навыки и понимание рынка для создания собственных моделей.
Спасибо за Ваш интерес.