В конце моей последней статьи я упомянул о своем страстном желании «увидеть» фондовые рынки через призму машинного обучения. В частности, я хотел найти способ извлечь уроки из исторических данных для прогнозирования будущих изменений. Прочитав практически все доступные публичные статьи на стыке машинного обучения и финансовых рынков, я наткнулся на книгу Лопеса Де Прадо «Достижения в области финансового машинного обучения». Я был впечатлен научной ортодоксальностью автора, что, безусловно, помогло мне сократить время моего обучения. Однако практически все самые изощренные методики, тщательно разработанные в книге, в моем случае не принесли результатов. Я должен был выяснить свой собственный набор функций.

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

Я бы сказал, что во время этого пути главной задачей было преодолеть высокое отношение шума к сигналу. Эффективные/умные участники рынка (умные деньги) используют различные уловки, чтобы замаскировать свои интервенции на рынках. Они постоянно производят фальшивые триггеры быков/медведей, чтобы ввести в заблуждение остальную часть рынка. Создайте неопределенность и используйте эту неопределенность для получения значительной прибыли с течением времени.

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

Мотивация:

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

Классические алгоритмы предлагают способ преодолеть эти проблемы. Они переводят собственное понимание рынка трейдером в автоматизированную торговую систему. Он может отслеживать колебания цен и выполнять заказы при обнаружении сигналов. В большинстве случаев они многословны, потому что мы должны включить все сценарии.

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

Мы дадим четкий ответ на этот вопрос в этой статье.

Сбор данных:

Нам необходимо собрать точный набор исторических данных (открытие, максимум, минимум, закрытие и объем) для каждой части исследования акций. Каждая акция демонстрирует разное поведение. Таким образом, у каждой акции будет своя модель машинного обучения.

Вы можете получить бесплатно исторические данные из разных источников:

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%. Я пробовал этот подход с разными классами активов. Каждый раз я получал стабильные результаты.

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

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

Спасибо за Ваш интерес.