Импульсная торговая стратегия

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

Предыдущее резюме

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

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

Тем не менее, мы можем использовать новый подход, вставляя модуль онлайн-обнаружения точек изменения (CPD) в конвейер Deep Momentum Network (DMN), который использует архитектуру глубокого обучения LSTM для одновременного изучения как оценки тренда, так и определения размера позиции. Кроме того, наша модель способна оптимизировать способ баланса, как видно из бумаги Вуда и команды

Медленный импульс с быстрым возвратом

Я прочитал исследовательскую работу Кирана Вуда, Стивена Робертса, Стефана Зохре «Медленный импульс с быстрым возвратом: торговая стратегия с использованием глубокого обучения и обнаружения точек изменения. Вуд и его команда представляют новый подход к вставке модуля обнаружения точек изменения (CPD) в конвейер Deep Momentum Network (DMN), который использует архитектуру глубокого обучения LSTM для одновременного изучения как оценки тренда, так и определения размера позиции.

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

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

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

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

При тестировании на истории в период с 1995 по 2020 год добавление модуля CPD привело к улучшению коэффициента Шарпа на одну треть. Модуль особенно полезен в периоды значительной нестационарности, и, в частности, за последние проверенные годы (2015–2020 гг.) прирост производительности составляет примерно две трети. Это интересно, поскольку традиционные импульсные стратегии были неэффективными в этот период

LSTM

Архитектура LSTM (Long Short-Term Memory) — это тип рекуррентной нейронной сети (RNN), которая, как было показано, эффективна для решения различных задач [1]. LSTM был разработан для решения проблем исчезающего и взрывающегося градиента, обычно встречающихся в RNN.

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

Размер торгового сигнала и позиции

Входными признаками являются нормализованные доходности со смещением по времени на 1, 21, 63, 126 и 256 дней, соответствующие ежедневным, ежемесячным, квартальным, двухгодичным и годовым отчетам. Кроме того, вводятся индикаторы MACD, которые представляют собой нормализованный по волатильности сигнал конвергенции-дивергенции скользящей средней, определяющий взаимосвязь между коротким и длинным сигналом. Индикаторы MACD используются в парах (8, 24), (16, 28) и (32, 96). Эти индикаторы можно рассматривать как выполняющие функцию, аналогичную сверточному слою.

Выходные данные модели представляют собой последовательность позиций, при этом только конечная позиция в последовательности имеет отношение к стратегии. За выводом LSTM следует распределенный по времени, полностью связанный слой с функцией активации tanh(·), которая является функцией сжатия, которая напрямую выводит позиции X(-1,1) Модель оптимизирована для метрик производительности с поправкой на риск, таких как коэффициент Шарпа, с использованием функции потерь Шарпа. Функция потерь Шарпа определяется как

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

Обучение с помощью программирования

Лучший способ понять — попробовать, так что давайте начнем.

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

# basic lib
from typing import Dict, List, Optional, Tuple, Union
import csv
import datetime as dt

import pandas as pd
import numpy as np

import yfinance as yf

# Model Lib
import gpflow
import tensorflow as tf
from gpflow.kernels import ChangePoints, Matern32
from sklearn.preprocessing import StandardScaler
from tensorflow_probability import bijectors as tfb
ticker = "^GSPC"

# Get the S&P 500 index data
spx = yf.Ticker(ticker)
# Download the daily data
data = spx.history(period="1000d", interval="1d")
data["daily_returns"] = data["Close"] / data["Close"].shift(day_offset) - 1.0

Модуль обнаружения точек изменения

Модуль обнаружения точек изменения — это модуль, который был вставлен в сетевой конвейер с глубоким импульсом. Это помогает обнаруживать резкие изменения рыночных тенденций, например, когда восходящий тренд разворачивается и становится нисходящим (или наоборот). Он использует EQUATION для всех временных шагов в LSTM, оглядываясь назад от времени T с шагами τ. Выходные данные этого модуля используются для балансировки как стратегий медленного импульса, так и стратегий быстрого возврата к среднему на основе данных.

Kernel = gpflow.kernels.base.Kernel
MAX_ITERATIONS = 200

Сигмоиды — это математические функции, которые принимают входные данные и выводят значение от 0 до 1. Их можно использовать для моделирования нелинейных зависимостей, таких как те, которые обнаруживаются в данных финансовых временных рядов. В этой исследовательской работе сигмовидные функции использовались, чтобы помочь сетевому конвейеру с глубоким импульсом быстрее адаптироваться к изменениям на рынке, обнаруживая точки изменения (т. е. резкие сдвиги) в наборе данных.

def _sigmoids(self, X: tf.Tensor) -> tf.Tensor:
# overwrite to remove sorting of locations
locations = tf.reshape(self.locations, (1, 1, -1))
steepness = tf.reshape(self.steepness, (1, 1, -1))
return tf.sigmoid(steepness * (X[:, :, None] - locations))

Древесина использует ядро Matern с дополнительными гиперпараметрами (входная шкала l, выходная шкала s h, уровень шума s n ), чтобы помочь уловить более постепенные переходы от одной ковариационной функции к другой при изменениях на рынке. Ядро Матерна — это тип ковариационной функции, используемой в гауссовских процессах.

def fit_matern_kernel(
time_series_data: pd.DataFrame,
variance: float = 1.0,
lengthscale: float = 1.0,
likelihood_variance: float = 1.0,
) -> Tuple[float, Dict[str, float]]:
m = gpflow.models.GPR(
data=(
time_series_data.loc[:, ["X"]].to_numpy(),
time_series_data.loc[:, ["Y"]].to_numpy(),
),
kernel=Matern32(variance=variance, lengthscales=lengthscale),
noise_variance=likelihood_variance,
)
opt = gpflow.optimizers.Scipy()
nlml = opt.minimize(
m.training_loss, m.trainable_variables, options=dict(maxiter=MAX_ITERATIONS)
).fun
params = {
"kM_variance": m.kernel.variance.numpy(),
"kM_lengthscales": m.kernel.lengthscales.numpy(),
"kM_likelihood_variance": m.likelihood.variance.numpy(),
}
return nlml, params

Соответствие ядра ChangePoint

def fit_changepoint_kernel(
time_series_data: pd.DataFrame,
k1_variance: float = 1.0,
k1_lengthscale: float = 1.0,
k2_variance: float = 1.0,
k2_lengthscale: float = 1.0,
kC_likelihood_variance=1.0,
kC_changepoint_location=None,
kC_steepness=1.0,
) -> Tuple[float, float, Dict[str, float]]:
if not kC_changepoint_location:
kC_changepoint_location = (
time_series_data["X"].iloc[0] + time_series_data["X"].iloc[-1]
) / 2.0

m = gpflow.models.GPR(
data=(
time_series_data.loc[:, ["X"]].to_numpy(),
time_series_data.loc[:, ["Y"]].to_numpy(),
),
kernel=ChangePointsWithBounds(
[
Matern32(variance=k1_variance, lengthscales=k1_lengthscale),
Matern32(variance=k2_variance, lengthscales=k2_lengthscale),
],
location=kC_changepoint_location,
interval=(time_series_data["X"].iloc[0], time_series_data["X"].iloc[-1]),
steepness=kC_steepness,
),
)
m.likelihood.variance.assign(kC_likelihood_variance)
opt = gpflow.optimizers.Scipy()
nlml = opt.minimize(
m.training_loss, m.trainable_variables, options=dict(maxiter=200)
).fun
changepoint_location = m.kernel.locations[0].numpy()
params = {
"k1_variance": m.kernel.kernels[0].variance.numpy().flatten()[0],
"k1_lengthscale": m.kernel.kernels[0].lengthscales.numpy().flatten()[0],
"k2_variance": m.kernel.kernels[1].variance.numpy().flatten()[0],
"k2_lengthscale": m.kernel.kernels[1].lengthscales.numpy().flatten()[0],
"kC_likelihood_variance": m.likelihood.variance.numpy().flatten()[0],
"kC_changepoint_location": changepoint_location,
"kC_steepness": m.kernel.steepness.numpy(),
}
return changepoint_location, nlml, params

Рассчитать оценку Changepoint

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

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

def fit_changepoint_kernel(
time_series_data: pd.DataFrame,
k1_variance: float = 1.0,
k1_lengthscale: float = 1.0,
k2_variance: float = 1.0,
k2_lengthscale: float = 1.0,
kC_likelihood_variance=1.0,
kC_changepoint_location=None,
kC_steepness=1.0,
) -> Tuple[float, float, Dict[str, float]]:
"""Fit the Changepoint kernel on a time-series

Args:
time_series_data (pd.DataFrame): time-series with ciolumns X and Y
k1_variance (float, optional): variance parameter initialisation for k1. Defaults to 1.0.
k1_lengthscale (float, optional): lengthscale initialisation for k1. Defaults to 1.0.
k2_variance (float, optional): variance parameter initialisation for k2. Defaults to 1.0.
k2_lengthscale (float, optional): lengthscale initialisation for k2. Defaults to 1.0.
kC_likelihood_variance (float, optional): likelihood variance parameter initialisation. Defaults to 1.0.
kC_changepoint_location (float, optional): changepoint location initialisation, if None uses midpoint of interval. Defaults to None.
kC_steepness (float, optional): steepness parameter initialisation. Defaults to 1.0.

Returns:
Tuple[float, float, Dict[str, float]]: changepoint location, negative log marginal likelihood and paramters after fitting the GP
"""
if not kC_changepoint_location:
kC_changepoint_location = (
time_series_data["X"].iloc[0] + time_series_data["X"].iloc[-1]
) / 2.0

m = gpflow.models.GPR(
data=(
time_series_data.loc[:, ["X"]].to_numpy(),
time_series_data.loc[:, ["Y"]].to_numpy(),
),
kernel=ChangePointsWithBounds(
[
Matern32(variance=k1_variance, lengthscales=k1_lengthscale),
Matern32(variance=k2_variance, lengthscales=k2_lengthscale),
],
location=kC_changepoint_location,
interval=(time_series_data["X"].iloc[0], time_series_data["X"].iloc[-1]),
steepness=kC_steepness,
),
)
m.likelihood.variance.assign(kC_likelihood_variance)
opt = gpflow.optimizers.Scipy()
nlml = opt.minimize(
m.training_loss, m.trainable_variables, options=dict(maxiter=200)
).fun
changepoint_location = m.kernel.locations[0].numpy()
params = {
"k1_variance": m.kernel.kernels[0].variance.numpy().flatten()[0],
"k1_lengthscale": m.kernel.kernels[0].lengthscales.numpy().flatten()[0],
"k2_variance": m.kernel.kernels[1].variance.numpy().flatten()[0],
"k2_lengthscale": m.kernel.kernels[1].lengthscales.numpy().flatten()[0],
"kC_likelihood_variance": m.likelihood.variance.numpy().flatten()[0],
"kC_changepoint_location": changepoint_location,
"kC_steepness": m.kernel.steepness.numpy(),
}
return changepoint_location, nlml, params

Мы нормализуем оценку точки изменения

def changepoint_severity(
kC_nlml: Union[float, List[float]], kM_nlml: Union[float, List[float]]
) -> float:
normalized_nlml = kC_nlml - kM_nlml
return 1 - 1 / (np.mean(np.exp(-normalized_nlml)) + 1)

Создание функций Momentum

Создание функций Momentum — важный шаг в конвейере сети с глубоким импульсом. Это включает в себя нормализацию значений с помощью EQUATION, которые являются нормализованными значениями, с ретроспективным горизонтом l из времени T. Это гарантирует, что мы можем предположить, что наше среднее значение по каждому окну равно нулю, и иметь большую согласованность во всех окнах при запуске модуля CPD.

После того, как мы выпустили модуль CDM, прежде чем передавать его в сетевой конвейер с глубоким импульсом, нам нужно создать некоторые функции Momemtum для обучения модели. В Github Кирана Вуда создайте следующие переменные:

  • Ежедневный том
  • Целевая доходность
  • Норма ежедневной доходности
  • Нормируйте ежемесячную доходность
  • Нормальная квартальная декларация
  • Норма двухгодичного дохода
  • Норма годовой доходности
  • MACD
  • День недели
  • День месяца
  • неделя года
  • Месяц года
  • год

Мы можем написать код следующим образом:

def deep_momentum_strategy_features(df_asset: pd.DataFrame) -> pd.DataFrame:
"""prepare input features for deep learning model
Args:
df_asset (pd.DataFrame): time-series for asset with column close
Returns:
pd.DataFrame: input features
"""

df_asset = df_asset[
~df_asset["Close"].isna()
| ~df_asset["Close"].isnull()
| (df_asset["Close"] > 1e-8) # price is zero
].copy()

# winsorize using rolling 5X standard deviations to remove outliers
df_asset["srs"] = df_asset["Close"]
ewm = df_asset["srs"].ewm(halflife=HALFLIFE_WINSORISE)
means = ewm.mean()
stds = ewm.std()
df_asset["srs"] = np.minimum(df_asset["srs"], means + VOL_THRESHOLD * stds)
df_asset["srs"] = np.maximum(df_asset["srs"], means - VOL_THRESHOLD * stds)

df_asset["daily_returns"] = calc_returns(df_asset["srs"])
df_asset["daily_vol"] = calc_daily_vol(df_asset["daily_returns"])
# vol scaling and shift to be next day returns
df_asset["target_returns"] = calc_vol_scaled_returns(
df_asset["daily_returns"], df_asset["daily_vol"]
).shift(-1)

def calc_normalised_returns(day_offset):
return (
calc_returns(df_asset["srs"], day_offset)
/ df_asset["daily_vol"]
/ np.sqrt(day_offset)
)

df_asset["norm_daily_return"] = calc_normalised_returns(1)
df_asset["norm_monthly_return"] = calc_normalised_returns(21)
df_asset["norm_quarterly_return"] = calc_normalised_returns(63)
df_asset["norm_biannual_return"] = calc_normalised_returns(126)
df_asset["norm_annual_return"] = calc_normalised_returns(252)

trend_combinations = [(8, 24), (16, 48), (32, 96)]
for short_window, long_window in trend_combinations:
df_asset[f"macd_{short_window}_{long_window}"] = MACDStrategy.calc_signal(
df_asset["srs"], short_window, long_window
)

# date features
if len(df_asset):
df_asset["day_of_week"] = df_asset.index.dayofweek
df_asset["day_of_month"] = df_asset.index.day
df_asset["week_of_year"] = df_asset.index.weekofyear
df_asset["month_of_year"] = df_asset.index.month
df_asset["year"] = df_asset.index.year
df_asset["date"] = df_asset.index # duplication but sometimes makes life easier
else:
df_asset["day_of_week"] = []
df_asset["day_of_month"] = []
df_asset["week_of_year"] = []
df_asset["month_of_year"] = []
df_asset["year"] = []
df_asset["date"] = []

return df_asset.dropna()

df_asset = deep_momentum_strategy_features(df_asset= data)

Вывод выглядит следующим образом:

Функции Merage CPD и функции Momentum.

features =  pd.concat([df_asset, cpd_features], axis=0)

Эксперимент DMN

Мы достигли кульминации этой статьи, It’s Model DMN Building

SharpeLoss

class SharpeLoss(tf.keras.losses.Loss):
def __init__(self, output_size: int = 1):
self.output_size = output_size # in case we have multiple targets => output dim[-1] = output_size * n_quantiles
super().__init__()

def call(self, y_true, weights):
captured_returns = weights * y_true
mean_returns = tf.reduce_mean(captured_returns)
return -(
mean_returns
/ tf.sqrt(
tf.reduce_mean(tf.square(captured_returns))
- tf.square(mean_returns)
+ 1e-9
)
* tf.sqrt(252.0)
)

LstmDeepMomentumNetworkModel

Настройка ввода

learning_rate = 
time_steps =
input_size =
hidden_layer_size =
dropout_rate =
input = keras.Input((time_steps, input_size))
lstm = tf.keras.layers.LSTM(hidden_layer_size,
return_sequences=True,
dropout=dropout_rate,
stateful=False,
activation="tanh",
recurrent_activation="sigmoid",
recurrent_dropout=0,
unroll=False,
use_bias=True,
)(input)

Настройка отсева

dropout = keras.layers.Dropout(dropout_rate)(lstm)

Настройка вывода

output = tf.keras.layers.TimeDistributed(
tf.keras.layers.Dense(
output_size,
activation=tf.nn.tanh,
kernel_constraint=keras.constraints.max_norm(3),
)(dropout[..., :, :])

Настройка параметра модели

# Model set
model = keras.Model(inputs=input, outputs=output)
adam = keras.optimizers.Adam(lr=learning_rate, clipnorm=max_gradient_norm)
sharpe_loss = SharpeLoss(self.output_size).call

Компиляция модели

# Model
model.compile(loss=sharpe_loss, optimizer=adam,sample_weight_mode="temporal", )

Заключительные мысли

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

Ссылка:

https://arxiv.org/pdf/2105.13727.pdf

Источник