Финансовое машинное обучение, часть 1: этикетки

Постановка задачи контролируемого обучения

Вступление

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

Как и в остальной части этой серии, методы, показанные в этом посте, основаны на Достижениях в области финансового машинного обучения Маркоса Лопеса де Прадо. Я рекомендую заглянуть в книгу для более подробного изучения этого предмета. С учетом сказанного, пора прыгнуть и поплавать.

Маркировка наблюдений

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

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

Этот метод маркировки - хорошее начало, но он имеет две решаемые проблемы.

  1. Пороги фиксированы, а волатильность - нет - это означает, что иногда наши пороги слишком далеко друг от друга, а иногда - слишком близко друг к другу. Когда волатильность низкая (например, во время ночной торговой сессии), мы получим в основном y = 0 меток, хотя низкая доходность предсказуема и статистически значима.
  2. Этикетка не зависит от пути, что означает, что она зависит только от доходности на горизонте, а не от промежуточной доходности. Это проблема, потому что этикетка неточно отражает реальность торговли - каждая стратегия имеет порог стоп-лосса и порог тейк-профита, которые позволяют закрыть позицию досрочно. Если промежуточная доходность достигает порога стоп-лосса, мы осознаем убыток, и удерживать позицию или получать прибыль от нее нереально. И наоборот, если промежуточная доходность достигает порога тейк-профита, мы закрываем ее, чтобы зафиксировать прибыль, даже если доходность на горизонте равна нулю или отрицательна.

Расчет динамических пороговых значений

Чтобы решить первую проблему, мы можем установить динамические пороги в зависимости от скользящей волатильности. Мы предполагаем, что на данный момент у нас уже есть полоски OHLC. Я использую долларовые столбики BitMex: XBT, контракт бессрочного свопа биткойнов из предыдущего поста - этот фрагмент кода поможет вам наверстать упущенное, если вы начинаете с нуля.

Здесь мы оценим почасовую волатильность доходности для вычисления пороговых значений прибыли и убытка. Ниже вы найдете слегка измененную функцию непосредственно от Lopez De Prado с комментариями, добавленными для ясности:

def get_vol(prices, span=100, delta=pd.Timedelta(hours=1)):
  # 1. compute returns of the form p[t]/p[t-1] - 1
  # 1.1 find the timestamps of p[t-1] values
  df0 = prices.index.searchsorted(prices.index - delta)
  df0 = df0[df0 > 0]
  # 1.2 align timestamps of p[t-1] to timestamps of p[t]
  df0 = pd.Series(prices.index[df0-1],    
           index=prices.index[prices.shape[0]-df0.shape[0] : ])
  # 1.3 get values by timestamps, then compute returns
  df0 = prices.loc[df0.index] / prices.loc[df0.values].values - 1
  # 2. estimate rolling standard deviation
  df0 = df0.ewm(span=span).std()
  return df0

Добавление зависимости пути: метод тройного барьера

Чтобы лучше учитывать сценарии стоп-лосса и тейк-профита гипотетической торговой стратегии, мы изменим метод маркировки с фиксированным горизонтом, чтобы он отражал, какой барьер был затронут первым - верхний, нижний или горизонт. Отсюда и название: трехбарьерный метод.

Схема маркировки определяется следующим образом:

y = 1 : первым поражается верхний барьер

y = 0 : правый барьер поражается первым

y = -1 : сначала поражается нижний барьер

Как насчет стороны ставки?

Приведенная выше схема отлично работает для длинных стратегий, однако все усложняется, когда мы учитываем как длинные, так и короткие ставки. Если мы делаем короткую ставку, наша прибыль / убыток инвертируется относительно движения цены - мы получаем прибыль, если цена снижается, и проигрываем, когда цена растет.

Чтобы учесть это, мы можем просто представить side как 1 для длинного и -1 для краткого. Таким образом, мы можем умножить нашу прибыль на побочную, так что всякий раз, когда мы делаем короткую ставку, отрицательная прибыль становится положительной и наоборот. Фактически, мы переворачиваем метки y = 1 и y = -1, если side = -1.

Давайте попробуем реализовать (на основе кода MLDP).

Сначала мы определяем процедуру получения отметок времени горизонтальных барьеров:

def get_horizons(prices, delta=pd.Timedelta(minutes=15)):
    t1 = prices.index.searchsorted(prices.index + delta)
    t1 = t1[t1 < prices.shape[0]]
    t1 = prices.index[t1]
    t1 = pd.Series(t1, index=prices.index[:t1.shape[0]])
    return t1

Теперь, когда у нас есть горизонтальные барьеры, мы определяем функцию для установки верхнего и нижнего барьеров на основе ранее вычисленных оценок волатильности:

def get_touches(prices, events, factors=[1, 1]):
  '''
  events: pd dataframe with columns
    t1: timestamp of the next horizon
    threshold: unit height of top and bottom barriers
    side: the side of each bet
  factors: multipliers of the threshold to set the height of 
           top/bottom barriers
  '''
  out = events[['t1']].copy(deep=True)
  if factors[0] > 0: thresh_uppr = factors[0] * events['threshold']
  else: thresh_uppr = pd.Series(index=events.index) # no uppr thresh
  if factors[1] > 0: thresh_lwr = -factors[1] * events['threshold']
  else: thresh_lwr = pd.Series(index=events.index)  # no lwr thresh
  for loc, t1 in events['t1'].iteritems():
    df0=prices[loc:t1]                              # path prices
    df0=(df0 / prices[loc] - 1) * events.side[loc]  # path returns
    out.loc[loc, 'stop_loss'] = \
      df0[df0 < thresh_lwr[loc]].index.min()  # earliest stop loss
    out.loc[loc, 'take_profit'] = \
      df0[df0 > thresh_uppr[loc]].index.min() # earliest take profit
  return out

Наконец, мы определяем функцию для вычисления меток:

def get_labels(touches):
  out = touches.copy(deep=True)
  # pandas df.min() ignores NaN values
  first_touch = touches[['stop_loss', 'take_profit']].min(axis=1)
  for loc, t in first_touch.iteritems():
    if pd.isnull(t):
      out.loc[loc, 'label'] = 0
    elif t == touches.loc[loc, 'stop_loss']: 
      out.loc[loc, 'label'] = -1
    else:
      out.loc[loc, 'label'] = 1
  return out

Собираем все вместе:

data_ohlc = pd.read_parquet('data_dollar_ohlc.pq')
data_ohlc = \
  data_ohlc.assign(threshold=get_vol(data_ohlc.close)).dropna()
data_ohlc = data_ohlc.assign(t1=get_horizons(data_ohlc)).dropna()
events = data_ohlc[['t1', 'threshold']] 
events = events.assign(side=pd.Series(1., events.index)) # long only
touches = get_touches(data_ohlc.close, events, [1,1])
touches = get_labels(touches)
data_ohlc = data_ohlc.assign(label=touches.label)

Мета-маркировка

На концептуальном уровне наша цель состоит в том, чтобы делать ставки там, где мы ожидаем выигрыша, а не делать ставки там, где мы не ожидаем выигрыша, что сводится к проблеме бинарной классификации (где проигрышный случай включает как ставки в неправильном направлении, так и мы вообще не делаем ставок, когда должны были). Вот еще один способ взглянуть на только что сгенерированные ярлыки:

Проблема двоичной классификации представляет собой компромисс между ошибками типа I (ложные срабатывания) и ошибками типа II (ложноотрицательные результаты). Увеличение количества истинных положительных результатов обычно происходит за счет увеличения количества ложных срабатываний.

Чтобы охарактеризовать это более формально, давайте сначала определим:

ŷ ∈ {0, 1, -1}: прогноз первичной модели для наблюдения

r: возврат цены за наблюдение

Тогда во время прогнозирования матрица неточностей первичной модели выглядит так, как показано ниже.

Нас не слишком беспокоят ложноотрицательные результаты - мы можем пропустить несколько ставок, но, по крайней мере, мы не теряем деньги. Больше всего нас беспокоят ложные срабатывания - здесь мы теряем деньги.

Чтобы отразить это, наши мета-метки y * могут быть определены согласно диаграмме:

y * = 1 : истинно положительный

y * = 0 : все, кроме истинно положительного

Фактически, первичная модель должна иметь высокую отзывчивость - она ​​должна правильно определять больше истинных положительных результатов за счет множества ложных срабатываний. Затем вторичная модель отфильтрует ложные срабатывания первой модели.

Реализация мета-маркировки

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

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

from imblearn.over_sampling import SMOTE
X = data_ohlc[['open', 'close', 'high', 'low', 'vwap']].values
y = np.squeeze(data_ohlc[['label']].values)
X_train, y_train = X[:4500], y[:4500]
X_test, y_test = X[4500:], y[4500:]
sm = SMOTE()
X_train_res, y_train_res = sm.fit_sample(X_train, y_train)

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

from sklearn.linear_model import LogisticRegression
clf = LogisticRegression().fit(X_train_res, y_train_res)
y_pred = clf.predict(X_test)

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

Давайте сопоставим наши трехбарьерные предсказания с бинарными положительными / отрицательными мета-метками, введенными ранее, и проверим матрицу путаницы:

def true_binary_label(y_pred, y_test):
    bin_label = np.zeros_like(y_pred)
    for i in range(y_pred.shape[0]):
        if y_pred[i] != 0 and y_pred[i]*y_test[i] > 0:
            bin_label[i] = 1  # true positive
    return bin_label
from sklearn.metrics import confusion_matrix
cm= confusion_matrix(true_binary_label(y_pred, y_test), y_pred != 0)

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

.

.

# generate predictions for training set
y_train_pred = clf.predict(X_train) 
# add the predictions to features 
X_train_meta = np.hstack([y_train_pred[:, None], X_train])
X_test_meta = np.hstack([y_pred[:, None], X_test])
# generate true meta-labels
y_train_meta = true_binary_label(y_train_pred, y_train)
# rebalance classes again
sm = SMOTE()
X_train_meta_res, y_train_meta_res = sm.fit_sample(X_train_meta, y_train_meta)
model_secondary = LogisticRegression().fit(X_train_meta_res, y_train_meta_res)
y_pred_meta = model_secondary.predict(X_test_meta)
# use meta-predictions to filter primary predictions
cm= confusion_matrix(true_binary_label(y_pred, y_test), (y_pred * y_pred_meta) != 0)

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

# WITHOUT META-LABELING
       label    precision    recall  f1-score   support

           0       1.00      0.66      0.79      1499
           1       0.14      1.00      0.24        81

   micro avg       0.67      0.67      0.67      1580
   macro avg       0.57      0.83      0.52      1580
weighted avg       0.96      0.67      0.76      1580
# WITH META-LABELING
       label    precision    recall  f1-score   support
       
           0       0.97      0.76      0.85      1499
           1       0.12      0.59      0.20        81

   micro avg       0.75      0.75      0.75      1580
   macro avg       0.55      0.68      0.53      1580
weighted avg       0.93      0.75      0.82      1580

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

Мета-маркировка: квантовый подход

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

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

Резюме

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