Стратегия возврата к среднему

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

Торговля по возврату к среднему значению

Изображение предоставлено HFT Research

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

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

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

Код: пошаговое руководство

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

Выбор акций

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

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

Начнем с кода. Для начала импортируем все необходимые библиотеки.# Import necessary libraries
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
from statsmodels.tsa.stattools import adfuller

Мы будем использовать yfinance для получения данных из Yahoo! Finance и from statsmodels.tsa.stattools import adfuller для вычисления расширенного теста Дики-Фуллера (ADF) для нашей вселенной акций.# Create a list of US stocks
stock_symbols = [
‘AAPL’, ‘MSFT’, ‘GOOGL’, ‘AMZN’, ‘TSLA’, ‘BRK-A’, ‘NVDA’,
‘JPM’, ‘JNJ’, ‘V’, ‘PG’, ‘UNH’, ‘MA’, ‘DIS’, ‘HD’, ‘BAC’, ‘VZ’,
‘INTC’, ‘KO’, ‘PFE’, ‘WMT’, ‘MRK’, ‘PEP’, ‘T’, ‘BA’, ‘XOM’, ‘ABBV’,
‘NKE’, ‘MCD’, ‘CSCO’, ‘DOW’, ‘ADBE’, ‘IBM’, ‘CVX’, ‘CRM’, ‘ABT’, ‘MDT’,
‘PYPL’, ‘NEE’, ‘COST’, ‘AMGN’, ‘CMCSA’, ‘NFLX’, ‘ORCL’, ‘PM’, ‘HON’, ‘ACN’,
‘TMO’, ‘AVGO’
]

# Fetch the data from Yahoo Finance
df = {}
for symbol in stock_symbols:
data = yf.download(symbol, start=’2015-01-01′, end=’2023-01-01′)
df[symbol] = data[‘Close’]

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

Далее мы будем использовать библиотеку yfinance, чтобы получить историческую цену выбранных нами акций. В этом случае я извлеку исторические данные с 2015-01-01 по 2023–01–01 2015–01–01.

Если вы хотите проверить исторические данные, вы можете использовать print(df) . Между тем, чтобы напечатать цены закрытия акций по отдельности для конкретной акции (например, ‘AAPL’): print(df[‘AAPL’].Close).

Проверка цены закрытия отдельных акций в Jupyter Notebook.

Двигаясь дальше, давайте закодируем расчет ADF:stationary_stocks = []
p_values = []

for symbol, data in df.items():
result = adfuller(data[‘Close’])
# A p-value less than 0.05 indicates that the data is stationary
p_value = result[1]
if p_value <= 0.05:
stationary_stocks.append(symbol)
p_values.append(p_value)

print(«Stocks suitable for mean reversion strategy:»)
for stock, p_value in zip(stationary_stocks, p_values): # Use zip to iterate over both lists simultaneously
print(f»Stock: {stock}, p-value: {p_value:.4f}»)

Предоставленный код оценивает акции на предмет потенциальных стратегий возврата к среднему значению с помощью расширенного теста Дики-Фуллера (ADF). Как упоминалось ранее, тест ADF оценивает, демонстрируют ли исторические цены закрытия акции стационарность, что является ключевой характеристикой для стратегий возврата к среднему значению.

В этом случае p-значение (вероятность) теста ADF сверяется с уровнем значимости 0,05. p-значение ниже этого порога говорит о том, что данные являются стационарными, что указывает на то, что цены на акции имеют тенденцию возвращаться к своему среднему значению с течением времени.

Код составляет список акций с p-значениями меньше или равными 0,05 в списке stationary_stocks, определяя те из них, которые являются потенциально подходящими кандидатами для стратегий возврата к среднему значению из-за их продемонстрированной тенденции к возврату к своим историческим средним значениям.

Выполнение приведенного выше кода приведет к следующему результату:

Как выясняется, из нашей вселенной акций. IBM является единственной акцией, у которой ежедневная цена закрытия демонстрирует свойства возврата к среднему значению с p-значением 0,0120.

Мы также можем визуализировать акцию, чтобы оценить ее тенденции к возврату к среднему значению, используя matplotlib.pylot со следующим кодом:def plot_stationary_stocks(df, stationary_stocks):
for stock in stationary_stocks:
data = df[stock].Close

# Calculate rolling statistics
rolling_mean = data.rolling(window=30).mean() # 30-day rolling mean
rolling_std = data.rolling(window=30).std() # 30-day rolling standard deviation

# Plot the statistics
plt.figure(figsize=(12, 6))
plt.plot(data, label=f'{stock} Prices’, color=’blue’)
plt.plot(rolling_mean, label=’Rolling Mean’, color=’red’)
plt.plot(rolling_std, label=’Rolling Std. Dev.’, color=’black’)
plt.title(f’Stationarity Check for {stock}’)
plt.xlabel(‘Date’)
plt.ylabel(‘Prices’)
plt.legend()
plt.grid(True)
plt.show()

# Calling the function
plot_stationary_stocks(df, stationary_stocks)

Проверка стационарности для IBM с 30-дневным скользящим средним и 30-дневным скользящим стандартным отклонением.

Разработка стратегии и тестирование на истории

Давайте перейдем к опробованию стратегии возврата к среднему значению на выбранной нами акции (IBM).

Для этого мы будем использовать возможности библиотеки backtesting.py — удобного и мощного инструментария для оценки торговых стратегий в экосистеме Python. Эта библиотека особенно ярко сияет, когда дело доходит до проверки простых стратегий, что делает ее идеальным выбором для таких сценариев. Более подробную информацию о backtesting.py вы можете найти на ее официальной странице: Backtesting.py — Тестирование торговых стратегий на Python (kernc.github.io).

Формулировка стратегииclass MeanReversion(Strategy):
n1 = 30 # Period for the moving average

def init(self):
# Compute moving average
self.offset = 0.01 # Buy/sell when price is 1% below/above the moving average
prices = self.data[‘Close’]
self.ma = self.I(self.compute_rolling_mean, prices, self.n1)

def compute_rolling_mean(self, prices, window):
return [(sum(prices[max(0, i — window):i]) / min(i, window)) if i > 0 else np.nan for i in range(len(prices))]

def next(self):
size = 0.1
# If price drops to more than offset% below n1-day moving average, buy
if self.data[‘Close’] < self.ma[-1] * (1 — self.offset):
if self.position.size < 0: # Check for existing short position
self.buy() # Close short position
self.buy(size=size)

# If price rises to more than offset% above n1-day moving average, sell
elif self.data[‘Close’] > self.ma[-1] * (1 + self.offset):
if self.position.size > 0: # Check for existing long position
self.sell() # Close long position
self.sell(size=size)

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

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

Вот объяснение ключевых компонентов:

  • MeanReversion является производным от класса Strategy, наследуя его функциональность. Этот класс служит основой для реализации стратегии возврата к среднему значению.
  • В методе init период n1 устанавливается равным 30 дням. Стратегия использует простую скользящую среднюю для оценки ценовых трендов.
  • Параметр offset определяется как 1%, указывая на то, что действия на покупку или продажу будут выполнены, когда цена отклонится на 1% ниже или выше скользящей средней.
  • Метод compute_rolling_mean вычисляет скользящее среднее цен закрытия за указанное окно. Вычисленные значения затем используются для определения скользящей средней.
  • next метод является ядром стратегии, выполняемой для каждой новой торговой точки данных. size переменной равен 0,1, что означает, что для каждой сделки, совершенной по данной стратегии, будет открываться или закрываться позиция на сумму, равную 10% от общего доступного капитала в портфеле.

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

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

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

Запуск тестирования на историиstock_to_backtest = stationary_stocks[0]
df = df[stock_to_backtest]
bt = Backtest(df, MeanReversion, cash=100000, commission=.002)
stats = bt.run()
bt.plot()

В приведенном выше коде класс Backtest используется для настройки среды тестирования на истории. Это делается путем создания экземпляра с именем bt, в котором исторические ценовые данные из DataFrame df объединяются со стратегией MeanReversion, которую мы сформулировали ранее. Дополнительные параметры, такие как первоначальный денежный капитал в размере 100 000 долларов США и комиссионный сбор в размере 0. Для симуляции тестирования на истории также указано 2% на сделку.

Результат тестирования на истории и интепретация

Посмотрим, как работает стратегия через сгенерированный график с помощью bt.plot().

График, сгенерированный из bt.plot()

Мы также можем вывести статистику со следующим кодомprint(stats)

Вот как будет выглядеть наша торговая статистикаStart 2014-12-31 00:00:00
End 2022-12-30 00:00:00
Duration 2921 days 00:00:00
Exposure Time [%] 99.851117
Equity Final [$] 231170.852727
Equity Peak [$] 235442.515106
Return [%] 131.170853
Buy & Hold Return [%] -8.145763
Return (Ann.) [%] 11.048887
Volatility (Ann.) [%] 23.417591
Sharpe Ratio 0.47182
Sortino Ratio 0.798583
Calmar Ratio 0.339264
Max. Drawdown [%] -32.567221
Avg. Drawdown [%] -3.396693
Max. Drawdown Duration 645 days 00:00:00
Avg. Drawdown Duration 42 days 00:00:00

# Trades 1613
Win Rate [%] 73.961562
Best Trade [%] 25.858214
Worst Trade [%] -15.540084
Avg. Trade [%] 1.976996
Max. Trade Duration 189 days 00:00:00
Avg. Trade Duration 42 days 00:00:00
Profit Factor 3.146867
Expectancy [%] 2.089253
SQN 7.474048
_strategy MeanReversion
_equity_curve …
_trades Size Entr…
dtype: object

Стратегия принесла положительную доходность в размере 131,17%, что соответствует годовой доходности в размере 11,05%. Для сравнения, стратегия «купи и держи» за тот же период привела бы к убыткам в размере 8,15%.

Ключевые метрики риска показывают максимальную просадку в 32,57%, что означает, что стратегия в своей худшей точке упала на 32,57% от своего пикового значения.

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

Что касается отдельных сделок, то стратегия совершила в общей сложности 1 613 сделок с высоким винрейтом 73,96%. Профит-фактор, представляющий собой отношение валовой прибыли к общему убытку, находится на благоприятном уровне 3,147, что означает, что прибыльных сделок стратегии было более чем в три раза больше, чем убыточных.

Источник