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

Благодаря современным достижениям в области машинного обучения и легкому доступу к данным в Интернете стало еще проще участвовать в количественной торговле. Чтобы сделать ситуацию еще лучше, облачные инструменты, такие как AWS, позволяют легко превращать торговые идеи в реальных, функциональных торговых ботов. Последние пару лет я возился с финансовыми данными и недавно решил сделать решительный шаг и создать полностью автоматизированную стратегию.

Стратегия

Поскольку это была моя первая автоматизированная стратегия, я решил сделать ее достаточно простой. Я бы торговал SPY (индекс S&P 500), используя обоснованные решения из модели ML. Я решил использовать SPY в качестве предпочтительного актива, поскольку он имеет относительно низкий риск, если что-то пойдет не так с моей стратегией.

Цель: Свинг-торговля SPY с использованием информированных точек покупки/продажи, определенных моделью машинного обучения.

Данные

Чтобы принимать обоснованные решения о том, когда торговать SPY, я обучил модель машинного обучения, которая будет прогнозировать сделки на основе ежедневных исторических данных из различных финансовых секторов и казначейских обязательств США. Я получил около 20 лет ежедневных данных по этим активам, используя API YFinance с открытым исходным кодом.

import yfinance as yf
import pandas as pd

SPY_daily = yf.download('SPY')
energy_daily = yf.download('XLE')
materials_daily = yf.download('XLB')
industrial_daily = yf.download('XLI')
utilities_daily = yf.download('XLU')
health_daily = yf.download('XLV')
financial_daily = yf.download('XLF')
consumer_discretionary_daily = yf.download('XLY')
consumer_staples_daily = yf.download('XLP')
technology_daily = yf.download('XLK')
real_estate_daily = yf.download('VGSIX')
TYBonds_daily = yf.download('^TNX')
VIX_daily = yf.download('^VIX')

Вот все данные, которые я извлекла для своей модели за последние 20 лет.

В блокноте Jupyter данные выглядели так, где каждая строка представляла собой отдельную дату.

Разработка функций

Затем, используя необработанные исторические данные, я создал дополнительные функции для своей модели. Я вывел различные индикаторы технического анализа, включая простые скользящие средние, волатильность и индекс относительной силы (RSI), и это лишь некоторые из них. Для большего разнообразия я рассчитал эти технические индикаторы с окнами 7, 20, 50 и 200 дней.

def SMA(df, feature, window_size):
    new_col = 'MA' + feature + str(window_size)
    df[new_col] = df[feature].rolling(window=window_size).mean()
    return df

def Volitility(df, feature, window_size):
    new_col = 'VOLITILITY' + feature + str(window_size)
    returns = np.log(df[feature]/df[feature].shift())
    returns.fillna(0, inplace=True)
    df[new_col] = returns.rolling(window=window_size).std()*np.sqrt(window_size)
    return df

def RSI(df, feature, window_size):
    new_col = 'RSI' + feature + str(window_size)
    delta = df[feature].diff()
    delta = delta[1:]
    up, down = delta.clip(lower=0), delta.clip(upper=0)
    roll_up = up.rolling(window_size).mean()
    roll_down = down.abs().rolling(window_size).mean()
    RS = roll_up / roll_down
    RSI = 100.0 - (100.0 / (1.0 + RS))
    df[new_col] = RSI
    return df

Маркировка данных

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

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

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

Метод тройного барьера

Метод тройного барьера — это интуитивно понятный способ маркировки финансовых данных для модели машинного обучения для прогнозирования результата сделки. Этот метод взят из книги Маркоса Лопеса де Прадо «Достижения в области финансового машинного обучения» (которую я очень рекомендую).

Метод тройного барьера работает следующим образом: во-первых, выберите временной интервал, в течение которого вы хотите удерживать свои сделки. Давайте сделаем это 100 торговых дней. Затем давайте определим идеальный порог тейк-профита для произвольной сделки. Давайте сделаем это 1x текущей волатильностью рынка. Наконец, давайте установим теоретический стоп-лосс для произвольной сделки. Для демонстрационных целей давайте также возьмем текущую рыночную волатильность за 1x. Эти три условия и есть наши «три барьера».

Теперь, когда мы установили наши барьеры, мы создаем метки для сделок SPY, используя следующие шаги:

1. Возьмите дату из финансового временного ряда, которую мы хотим пометить.

2. Идите на 100 торговых дней раньше намеченной даты. Создайте здесь вертикальный барьер. Этот барьер означает, что сделка удерживается слишком долго без стоп-ордера или достижения тейк-профита.

3. Создайте горизонтальный барьер в 1 раз выше нашей даты. Этот барьер означает, что сделка достигла порога тейк-профита.

4. Создайте еще один горизонтальный барьер на 1-кратную волатильность рынка ниже нашей даты. Этот барьер показывает, была ли сделка закрыта по стопу.

5. Отметьте нашу дату категорически на основе того, достигнет ли SPY барьер тейк-профита, достигнет ли барьер стоп-лосса или будет ли удерживаться слишком долго.

Наш конечный результат для одной даты будет выглядеть примерно так:

В приведенном выше примере показан метод тройного барьера для сделки в день 02–2017. Эта сделка не достигла нашего порога прибыли или стопа, а вместо этого сначала достигла вертикального барьера.

Для моей стратегии я изменил метод тройного барьера, чтобы использовать только две метки вместо трех меток. Если теоретическая сделка на определенную дату достигла моего порога тейк-профита, я давал ей ярлык «прибыль», а если нет, я давал ей «без прибыли». Это сделало бы мои данные пригодными для обучения модели бинарной классификации, которую было бы намного проще настроить. Я также настроил порог тейк-профита на 2x текущую рыночную волатильность, а максимальный период удержания всего на 10 дней, чтобы моя стратегия работала. быстрее.

Обозначение моих данных таким образом помогло бы обучить мою модель работать только с сделками с более быстрыми и большими выплатами.

def get_Daily_Volatility(close,span0=20):
    # simple percentage returns
    df0=close.pct_change()
    # 20 days, a month EWM's std as boundary
    df0=df0.ewm(span=span0).std()
    df0.dropna(inplace=True)
    return df0

def get_3_barriers(daily_volatility, price):
    #create a container
    barriers = pd.DataFrame(columns=['days_passed', 
              'price', 'vert_barrier', \
              'top_barrier', 'bottom_barrier'], \
               index = daily_volatility.index)

    for day, vol in daily_volatility.iteritems():
        days_passed = len(daily_volatility.loc[daily_volatility.index[0] : day])
        #set the vertical barrier 
        if (days_passed + t_final < len(daily_volatility.index) and t_final != 0):
            vert_barrier = daily_volatility.index[days_passed + t_final]
        else:
            vert_barrier = np.nan
        #set the top barrier
        if upper_lower_multipliers[0] > 0:
            top_barrier = prices.loc[day] + prices.loc[day] * upper_lower_multipliers[0] * vol
        else:
            #set it to NaNs
            top_barrier = pd.Series(index=prices.index)
        #set the bottom barrier
        if upper_lower_multipliers[1] > 0:
            bottom_barrier = prices.loc[day] - prices.loc[day] * upper_lower_multipliers[1] * vol
        else: 
            #set it to NaNs
            bottom_barrier = pd.Series(index=prices.index)
        barriers.loc[day, ['days_passed', 'price', 'vert_barrier','top_barrier', 'bottom_barrier']] = \
            days_passed, prices.loc[day], vert_barrier, \
            top_barrier, bottom_barrier

    return barriers

def get_labels(barriers):

    labels = []
    size = [] # percent gained or lossed 

    for i in range(len(barriers.index)):
        start = barriers.index[i]
        end = barriers.vert_barrier[i]
        if pd.notna(end):
            # assign the initial and final price
            price_initial = barriers.price[start]
            price_final = barriers.price[end]
            # assign the top and bottom barriers
            top_barrier = barriers.top_barrier[i]
            bottom_barrier = barriers.bottom_barrier[i]
            #set the profit taking and stop loss conditons
            condition_pt = (barriers.price[start: end] >= top_barrier).any()
            condition_sl = (barriers.price[start: end] <= bottom_barrier).any()
            #assign the labels
            if condition_pt: 
                labels.append(1)
            else: 
                labels.append(0)
            size.append((price_final - price_initial) / price_initial)
        else:
            labels.append(np.nan)
            size.append(np.nan)

    return labels, size

# how many days we hold the stock which set the vertical barrier
t_final = 10
#the up and low boundary multipliers
upper_lower_multipliers = [2, 2]
#allign the index

vol_df = get_Daily_Volatility(full_df.SPY_Close)
prices = full_df.SPY_Close[vol_df.index]
barriers = get_3_barriers(vol_df, prices)
barriers.index = pd.to_datetime(barriers.index)
labs, size = get_labels(barriers)
full_df = full_df[full_df.index.isin(barriers.index)]

Моделирование

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

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

Подготовка данных

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

def percentage_change(initial,final):
    return ((final - initial) / initial)

def expand_features(full_df):

    window = 100

    new_df = pd.DataFrame()
    for col in full_df.columns:
        print(col)
        if not col.startswith('label'):
            column = full_df[col]
            for i in range(1, window):
                shifted = column.shift(i)
                new_df['Shifted' + str(i) + col] = percentage_change(shifted, column)
        else:
            new_df[col] = full_df[col]

    return new_df

full_df = expand_features(full_df)

Данные, загружаемые в мою модель Catboost, теперь выглядели следующим образом: каждый столбец расширен, чтобы содержать процентное изменение по сравнению с x днями в прошлом. 20 301 уникальная функция теперь вводилась в модель, представляя широкий спектр исторических рыночных показателей.

Модель Catboost

Затем данные были вписаны в следующую модель, чтобы предсказать, приведет ли покупка SPY в каждую дату к прибыльной сделке.

classification_params = {'loss_function':'Logloss',
          'eval_metric':'AUC',
          'early_stopping_rounds': 2,
          'verbose': 200,
          'random_seed': SEED
         }

    model = CatBoostClassifier(**classification_params)
    model.fit(X_train, Y_train, 
            eval_set=(X_test, Y_test), 
            use_best_model=True, )

Анализ модели

Перекрестная проверка

Чтобы проверить свою модель, я разделил свой набор данных на набор поездов и тестовый набор «вне выборки». Набор данных поезда состоял из данных за март 2000 – август 2020 года, а мой тестовый набор состоял из данных за август 2020 – ноябрь 2022 года.

Перекрестная проверка KFold использовалась для определения производительности модели с использованием моих обучающих данных. Это включало в себя тренировочные данные, разделенные на 5 различных сегментов. Затем 4 из 5 сегментов менялись для обучения модели, а 1 сегмент использовался для проверки и точной настройки производительности модели.

Производительность модели

Как только я был уверен в своей архитектуре модели, я обучил окончательную модель, используя весь набор обучающих данных, и протестировал производительность вне выборки на моих тестовых данных.

Результаты показали, что модель оказалась намного лучше, чем я ожидал, с тестом ROC AUC, равным 0,69. Хотя это и не удивительный показатель ROC, я был очень впечатлен, увидев такой результат с финансовыми данными. Финансовые данные невероятно случайны, и трудно предсказать результаты. Все, что лучше подбрасывания монеты, считается преимуществом в нашей торговой стратегии.

Модель оказалась невероятно переобученной с тренировочным ROC AUC, равным 0,98, намного лучше, чем производительность на тестовых данных ,69. Честно говоря, я не слишком беспокоился об этом переоснащении, поскольку эта модель явно давала преимущество и выглядела намного лучше, чем предположения. Однако я думаю, что определенно есть больше возможностей обобщить эту модель, чтобы предотвратить переоснащение. Я считаю, что это можно сделать, включив больше данных или используя ансамбль пакетов, такой как случайный лес, чтобы соответствовать моим данным.

Анализ функций

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

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

Краткое содержание

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

Входные данные: историческая финансовая информация на дату во времени.

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

Тестирование на истории

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

Установка оптимального порога

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

Я мог бы сделать это проще и просто сказать: «покупайте SPY, если модель дает любую вероятность выше 50 %». Однако это обычно не лучший случай для модели бинарной классификации, поскольку мы должны учитывать риск как ложноположительных, так и ложноотрицательных результатов. Вместо этого я определил оптимальный порог, выяснив, какая выходная вероятность дает наибольший F Score на наших обучающих данных. К счастью, в Python это было невероятно просто! Оптимальный торговый порог оказался 0,46.

from sklearn.metrics import precision_recall_curve, f1_score
import numpy as np

#Create a Precision/Recall curve for our training data
precision_train, recall_train, pr_thresholds_train = 
    precision_recall_curve(Y_train, probabilities_train)
fscore_train = 
    2 * (precision_train * recall_train) / (precision_train + recall_train)

#Find optimal thresh on PR curve train
ix = np.argmax(fscore_train)
optimal_threshold = pr_thresholds_train[ix]

Выполнение бэктеста

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

Стратегия включала свинг-трейдинг SPY и выглядела так:

  1. Извлеките финансовые данные на конкретную дату, создайте необходимые функции и загрузите их в нашу модель.
  2. Покупайте SPY, если наша модель дает вероятность больше, чем 46%
  3. Настройте нашу сделку так, чтобы тейк-профит был равен удвоенной текущей рыночной волатильности, и установите разумный стоп-лосс для ограничения риска.

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

Тестирование на исторических данных показало невероятно хорошие результаты на данных вне выборки и принесло доход 50% в течение нашего периода времени. Для сравнения, общая рыночная доходность составляет всего 12%. Наша стратегия также приносила стабильную прибыль с небольшой просадкой.

Я также рассчитал некоторые общие статистические данные, чтобы оценить эффективность нашей стратегии за период тестирования, используя теоретический начальный баланс 10 000 долларов США.

Общий доход: 50,97 %

Всего сделок: 284

Общая чистая прибыль: 5107,70 долларов США.

Фактор прибыли: 2,67.

Процент прибыльных сделок: 46,47%

Средняя чистая прибыль от торговли: 17,98 долларов США.

Максимальная просадка: -554,87 долл. США

Удивительно, но наша стратегия имела только 46,47% всех сделок быть прибыльной. Однако, поскольку модель пытается предсказать большие восходящие движения акций, выигрышные сделки значительно компенсируют наши проигрышные сделки. Даже при большом количестве убыточных сделок средняя чистая прибыль составляла 17,98 долл. США.

Инфраструктура

Чтобы реализовать свою стратегию, я использовал AWS Cloud Development Kit (CDK) для создания инфраструктуры для размещения своей стратегии. Затем стратегия была настроена для взаимодействия с Alpaca (https://alpaca.markets/), API для торговли акциями и криптовалютами.

Для начала я сохранил свою модель в AWS S3, простом решении для хранения в облаке. Затем я создал две отдельные функции Lambda: функция покупки и функция продажи. Функция покупки будет запускаться в конце каждого торгового дня, генерировать предсказание модели о том, стоит ли совершать сделку, и настраивать сделки с помощью API Alpaca. Затем функция продажи будет постоянно сканировать открытые сделки, чтобы увидеть, достигли ли они порога, чтобы закрыть их, либо потому, что они достигли стоп-лосса, порога прибыли, либо удерживались более 10 дней. Наконец, я создал две таблицы DynamoDB: таблицу сделок, в которой будут отслеживаться текущие открытые сделки, и таблицу исторических сделок для отслеживания исторических сделок.

Связывание всего вместе

Перенесемся в наши дни: я торгую на бумаге по своей стратегии в брокерской компании Alpaca около 6 месяцев. Он работал примерно так же, как и результаты бэктеста, принося стабильную прибыль с небольшой просадкой. На данный момент он вырос на 6% с момента создания. Будет интересно посмотреть, как он будет работать с течением времени.

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

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

Надеюсь, вам понравилась эта статья, и, пожалуйста, не стесняйтесь воровать мои идеи!