Рынок как проблема кластеризации

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

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

Человеческая интуиция мало что может сказать о рынке. Да, в 2021 году был очень бычий рынок, но как именно определить «бычий» для 2021 года? Являются ли повышения цен спорадическими скачками или они более стабильны и последовательны? Несмотря на кажущуюся тривиальность, различия в этих двух сценариях могут привести к тому, что инвесторы разработают совершенно разные стратегии, и идеальный вариант в одном может значительно отличаться от другого.

В этом посте меня интересует этот 2023 год. Столкнувшись с вихревой непредсказуемостью, вызванной событиями 2021 и 2022 годов, попытка разделить 2023 год на его отличительные рыночные режимы и сравнить результаты с реальностью кажется серьезной проблемой для изучения.

2023, подстановочный знак

Бурная первая половина 2023 года вполне может быть следствием дестабилизирующих последствий агрессивного повышения процентных ставок в конце 2022 года (разговоры о котором еще продолжаются) вкупе с крахом криптовалютной биржи FTX в ноябре- Декабрь". С тех пор произошло множество дополнительных беспрецедентных событий, которые потрясли финансовый ландшафт — среди них был крах «Silicon Valley Bank (SVB) 10 марта, за которым быстро последовало крах Silvergate Bank и Signature Bank (за ним последовали новые крахи).

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

Разрушение процесса кластеризации

Процесс кластеризации, которому я следовал в этом посте, выглядит так:

  1. Получите данные о ценах на публичные акции, которые служат прокси для фондового рынка, а затем рассчитайте ежедневную доходность на основе данных о ценах.
  2. Вычислите функции, которые характеризуют заданный режим, исходя из возвращаемых данных.
  3. Масштабируйте и стандартизируйте функции, чтобы подготовить их к кластеризации.
  4. Предположите 2 кластера на 2023 год (K-Means требует, чтобы пользователь указал параметр num_clusters (количество кластеров) перед его обучением), обучите модель KMeans с помощью функций и пометьте ценовые данные с помощью идентифицированных значений кластера.
  5. Интерпретируйте, что означает каждый кластер по отношению к выбранным функциям.
  6. Визуализируйте ряд цен SPY, но сегментируйте ряды по разным режимам разными цветами.

Постановка проблемы

Чтобы сгруппировать 2023 год по режимам, нам сначала нужны данные о ценах. Я использую данные о ценах SPY ETF в качестве прокси для фондового рынка в целом, а затем вычисляю его ежедневную доходность по ценам.

Примечание. Функция get_pricing является оболочкой метода .history класса Ticker класса yfinance.

# FETCHING STOCK PRICES AND RETURNS DATA.
# ==============================================================================================================

from data_functions import get_pricing

start = '2023-01-01'
end = '2023-07-05'
ticker = 'SPY'

prices = get_pricing(ticker, start=start, end=end)
returns = np.log(prices).diff()
returns.head()

Выбор функции

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

  1. Возвращает
  2. Скользящее стандартное отклонение доходности
  3. Скользящая средняя доходность
  4. Движущаяся асимметрия доходности
  5. Движущийся эксцесс возврата
  6. Движущаяся автокорреляция доходностей
  7. Прыгать

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

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

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

# FEATURE ENGINEERING.
# ==============================================================================================================

def jump_condition(data, std_threshold):
    return ((data['returns'] > data['ma'] + std_threshold * data['volatility']) | 
            (data['returns'] < data['ma'] - std_threshold * data['volatility'])).astype(int)

def autocorr(data, window, autocorr_lag):
    return data['returns'].rolling(window).apply(lambda x: x.autocorr(lag=autocorr_lag), raw=False)

def engineer_features(returns, window=20, std_threshold=2, autocorr_lag=2):
    data = returns.copy()
    data['volatility'] = data['returns'].rolling(window).std()
    data['ma'] = data['returns'].rolling(window).mean()
    data['skew'] = data['returns'].rolling(window).skew()
    data['kurt'] = data['returns'].rolling(window).kurt()
    data['autocorr'] = autocorr(data, window, autocorr_lag)
    data['jump'] = jump_condition(data, std_threshold)
    return data[window:]
# --------------------------------------------------------------------------------------------------------------

window = 20
std_threshold = 2
autocorr_lag = 2
data = engineer_features(returns, window=window, std_threshold=std_threshold, autocorr_lag=autocorr_lag)
data[data['jump'] == 1].head()

Масштабирование функций

Теперь, когда у нас есть готовые функции, нам нужно масштабировать и стандартизировать их. Это обеспечивает 1) единообразие — когда функции имеют разные масштабы, те, у которых более крупные масштабы, могут непропорционально влиять на модель и 2) эффективность алгоритма — многие алгоритмы машинного обучения, включая K- Значит, используйте расчёты на основе расстояний. Стандартизированные функции помогают этим алгоритмам быстрее сходиться и работать точнее, поскольку они одинаково обрабатывают все измерения.

Чтобы масштабировать наши функции, мы используем StandardScaler from sklearn.processing:

from sklearn.preprocessing import StandardScaler

# SCALE DATA FOR CLUSTERING.
# ==============================================================================================================

def scale(data):
    scaler = StandardScaler()
    binary_ommitted_df = data[[column for column in data.columns if column != 'jump']] # Omit the jump variable.
    scaled_data = scaler.fit_transform(np.array(binary_ommitted_df).T)
    jump_feature = np.array(data['jump']).reshape(-1, 1)
    return np.concatenate((scaled_data.T, jump_feature), axis=1) # Re-append the jump variable.

# --------------------------------------------------------------------------------------------------------------

scaled_data = scale(data) 
pd.DataFrame(scaled_data).head() # To keep scaled_data in its array form.

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

Кластеризация

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

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

# CONDUCT CLUSTERING.
# ==============================================================================================================

from sklearn.cluster import KMeans

def kmeans_cluster(data, scaled_data, n_clusters, random_state, n_init='auto'):
    kmeans = KMeans(n_clusters=n_clusters, random_state=random_state, n_init=n_init)
    kmeans.fit(scaled_data)
    data['kmeans_regime'] = kmeans.labels_
    return data

# --------------------------------------------------------------------------------------------------------------

scaled_data = scale(data)
n_clusters = 2 # Two clusters hypothesized.
random_state = 42 # For reproducability.
data = kmeans_cluster(data, scaled_data, n_clusters, random_state)
data.head()

Характеристика и интерпретация режимов

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

# COMPUTE FEATURE AVGS
# ==============================================================================================================

feature_avgs = data.groupby('kmeans_regime').mean()
display(feature_avgs)

Исходя из вышеизложенного, мы можем понять следующее:

Режим 0. Этот режим характеризуется более низкой доходностью и волатильностью, что предполагает более медленный и более стабильный рынок. Несмотря на более низкую дневную доходность, положительная скользящая средняя указывает на постепенный восходящий тренд цены. Положительная асимметрия и эксцесс предполагают случайную более высокую доходность и резкие изменения цен. Отрицательная автокорреляция подразумевает тенденцию к развороту тренда, и время от времени наблюдаются заметные ценовые сдвиги, на которые указывают ненулевые «скачки».

Режим 1.Напротив, Режим 1 демонстрирует более высокую доходность и волатильность, что указывает на более динамичный и рискованный рынок. Несмотря на большую дневную доходность, более низкая доходность по скользящей средней предполагает меньшую устойчивость положительного тренда. Отрицательная асимметрия и эксцесс указывают на случайную более низкую доходность и меньшую вероятность резких изменений цен. Как и в режиме 0, существует тенденция к развороту тренда, о чем свидетельствует отрицательная автокорреляция. Однако отсутствие «скачков» предполагает более плавные и менее резкие изменения цен.

Построение цен SPY по режимам

Давайте теперь построим ценовой ряд SPY, а также раскрасим ряд в зависимости от того, какой режим когда возникает.

# PLOT SPY PRICES BY REGIME.
# ==============================================================================================================

def plot_regime(prices, data, s=10):
    
    _, ax = plt.subplots(figsize=(5, 3))
    for regime in data['kmeans_regime'].unique():
        subset = data[data['kmeans_regime'] == regime]
        ax.scatter(x=subset.index, y=prices[subset.index], label=f'Regime {regime}', s=s)
    ax.set_title(f"Prices Colored by K-Means Clustering Regimes")
    ax.set_ylabel('Price')
    ax.tick_params(axis='x', rotation=30)
    ax.legend()

    plt.tight_layout()
    plt.show()

# --------------------------------------------------------------------------------------------------------------

plot_regime(prices, data)

Теперь мы можем легко увидеть, когда в 2023 году произошли два режима. Давайте попробуем объяснить результаты нашего алгоритма K-средних траекторией развития событий в этом году.

2023 год: бремя банковских банкротств

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

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

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

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

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

Заключение

В заключение: кластеризация K-средних оказалась мощным инструментом в делении 2023 года на отдельные рыночные режимы. Ему удалось определить периоды хаоса и спокойствия и связать их со знаковыми событиями, такими как досадный (и, казалось бы, мгновенный) отказ SVB.

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

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

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

Привет, я Аун Си. Я исследую причуды фондового рынка с помощью программирования и пишу о своих выводах. Если вы хотите увидеть более детализированные версии моих сообщений в блоге, вы можете найти их в моем репозитории GitHub здесь. Сообщения в блоге стремятся быть более удобоваримыми/доступными, чем их аналоги из Jupyter Notebook в моем репозитории, поэтому могут быть некоторые части, которые, кажется, затушевывают определенные концепции, которые я исследовал, а также варианты и предположения, которые я делал на каждом этапе.

Не стесняйтесь обращаться по адресу [email protected]!