Руководство по тестированию и оптимизации торговых стратегий с помощью Vectorbt

Прибывайте в мир трейдинга, где успех заключается в умении определять рыночные тенденции и совершать прибыльные сделки, когда появляются возможности. Инструменты технического анализа, такие как Moving Average Convergence Divergence (MACD), могут помочь трейдерам определить потенциальные сигналы на покупку и продажу. Однако для достижения долгосрочной прибыльности трейдеры должны эффективно применять эти инструменты, что требует тщательной оценки и оптимизации.

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

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

Vectorbt — это высокопроизводительная среда тестирования на истории, состоящая из масштабируемых, компонуемых и расширяемых векторизованных инструментов тестирования и анализа на основе панд. Он позволяет использовать Pandas DataFrames и Series для моделирования, анализа и выполнения сложных торговых стратегий.

Для начала импортируем необходимые библиотеки:

import numpy as np
import pandas as pd
import scipy.stats as stats
import kaleido
import vectorbt as vbt
from itertools import combinations, product

Конфигурация портфеля

Далее мы установим параметры для нашей проверки продвижения вперед. Мы будем использовать 30 окон, каждое из которых длится 2 года и имеет 180 дней для тестирования:

# WalkForward Split params - 30 windows, each 2 years long, 180 days for test
split_kwargs = dict(
n=20,
window_len=365 * 2,
set_lens=(180,),
left_to_right=False
)

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

# Stop Loss - Not included in this example
# stop = 0.05 # 5%

# Backtesting Portfolio params
pf_kwargs = dict(
init_cash=100., # 100$ initial cash
slippage=0.001, # 0.1%
fees=0.001, # 0.1%
freq='d',
# direction='both', # long and short
# sl_stop=stop, # Stop Loss
# sl_trail=True, # Trailing Stop Loss
)

Мы определим пространство гиперпараметров для нашей стратегии MACD. Мы будем использовать сигнал 49 fast x 49 slow x 19. Мы также можем добавить два дополнительных параметра (не включенных в этот пример), macd_ewm, которые будут логическим значением, определяющим, следует ли использовать экспоненциальные скользящие средние для индикатора MACD:

# Define hyper-parameter space -> 49 fast x 49 slow x 19 signal. To Add: X 2 macd_ewm (np.array([True, False], dtype=bool))
fast_windows, slow_windows, signal_windows = vbt.utils.params.create_param_combs(
(product, (combinations, np.arange(10, 40, 1), 2), np.arange(10, 21, 1)))

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

# Get BTC Close price from Yahoo Finance
price = vbt.YFData.download('BTC-USD').get('Close') # Valid intervals: [1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo]

Теперь, когда у нас есть данные о ценах, давайте посмотрим на эволюцию цен. Мы можем сделать это с помощью метода plot()

# Price evolution
price.vbt.plot().show_png()

Мы также можем создавать скользящие сплиты для проверки с помощью метода rolling_split()

# Rolling splits for Walk Forward Validation
price.vbt.rolling_split(**split_kwargs, plot=True).show_svg()

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

# Walk Forward Validation Split
(in_price, in_indexes), (out_price, out_indexes) = price.vbt.rolling_split(**split_kwargs)

print(in_price.shape, len(in_indexes)) # in-sample
print(out_price.shape, len(out_indexes)) # out-sample
(550, 20) 20
(180, 20) 20

Построение торговой стратегии

Мы будем использовать индикатор MACD для построения нашей торговой стратегии. Индикатор MACD доступен в библиотеке VectorBT и может быть рассчитан с помощью vbt.MACD.run() В vbt.MACD.run() использует fast_windowslow_windowи signal_window для определения количества периодов для быстрой, медленной и сигнальной скользящих средних соответственно.

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

def simulate_all_params_MACD(price, fast_windows, slow_windows, signal_windows, **kwargs):
# Run MACD indicator
macd_ind = vbt.MACD.run(price, fast_window=fast_windows, slow_window=slow_windows, signal_window=signal_windows)

# Entry when MACD is above zero AND signal
entries = macd_ind.macd_above(0) & macd_ind.macd_above(macd_ind.signal)

# Exit when MACD is below zero OR signal
exits = macd_ind.macd_below(0) | macd_ind.macd_below(macd_ind.signal)

# Build portfolio
pf = vbt.Portfolio.from_signals(price, entries, exits,**kwargs)

# Draw all window combinations as a 3D volume
fig = pf.sharpe_ratio().vbt.volume(
x_level='macd_fast_window',
y_level='macd_slow_window',
z_level='macd_signal_window',
slider_level='split_idx',
trace_kwargs=dict(
colorbar=dict(
title='Sharpe Ratio',
)
)
)
fig.show()

return pf

Мы определяем функцию simulate_all_params_MACD() для запуска индикатора MACD для диапазона значений fast_window, slow_window и signal_window и моделируем длинную стратегию, используя полученные сигналы. Функция принимает следующие аргументы:

  • price: Данные о цене актива.
  • fast_windows: Список размеров окон быстрой скользящей средней для тестирования.
  • slow_windows: Список размеров окон медленной скользящей средней для тестирования.
  • signal_windows: Список размеров окон скользящей средней сигнала для тестирования.
  • **kwargs: Дополнительные аргументы для передачи в vbt. Portfolio.from_signals(), такие как initial_cash, сборы и проскальзывание.

Затем мы определяем сигналы входа и выхода на основе индикатора MACD, как описано выше. Мы используем vbt. Portfolio.from_signals() для моделирования стратегии только в долгосрочной перспективе и вычисления результирующих метрик. Наконец, мы визуализируем коэффициент Шарпа каждой комбинации гиперпараметров с помощью объемного 3D-графика.

После того, как мы определили нашу simulate_all_params_MACD функцию, мы можем вызвать ее на любой период. Здесь мы вызываем его для in_price с помощью следующего кода.

# Simulate all params for in-sample ranges
in_pf = simulate_all_params_MACD(in_price, fast_windows, slow_windows, signal_windows, **pf_kwargs)

Получение наилучших гиперпараметров для каждого сплита

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

# Get Best index - max performance (Sharpe Ratio) by each split
def get_best_index(performance, higher_better=True):
if higher_better:
return performance[performance.groupby('split_idx').idxmax()].index
return performance[performance.groupby('split_idx').idxmin()].index

in_best_index = get_best_index(in_pf.sharpe_ratio())

Здесь мы определили, get_best_index возвращать комбинацию гиперпараметров, которые дают наибольший коэффициент Шарпа в течение каждого периода. Мы определяем его, вызывая метод idxmax на in_pf.sharpe_ratio df, сгруппированном по номеру разделения и примененном к его индексу. В результате получается многоиндексный ряд, содержащий индекс пика коэффициента Шарпа для каждого сплита. Затем мы используем эту серию для выбора наилучших параметров.

# Get Best params
def get_best_params(best_index, level_name):
return best_index.get_level_values(level_name).to_numpy()

# Get best params from level values
in_best_fast_windows = get_best_params(in_best_index, 'macd_fast_window')
in_best_slow_windows = get_best_params(in_best_index, 'macd_slow_window')
in_best_signal_windows = get_best_params(in_best_index, 'macd_signal_window')

Приведенный выше код определяет get_best_params как простую функцию, которая возвращает значения level_name ряда best_index, запрошенные get_level_values и преобразованные в массивы numpy.

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

# Show best
in_best_window_combs = np.array(list(zip(in_best_fast_windows, in_best_slow_windows, in_best_signal_windows)))
pd.DataFrame(in_best_window_combs, columns=['fast_window', 'slow_window', 'signal_window']).vbt.plot().show_svg()

Приведенный выше код создает массив наилучших комбинаций гиперпараметров, а затем строит их на графике с помощью функции vbt.plot()

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

# First five best combinations
in_best_index[:5]

Здесь мы напечатали первые пять строк:

MultiIndex([(13, 31, 10, 0),
(11, 32, 10, 1),
(10, 37, 14, 2),
(10, 31, 19, 3),
(16, 19, 18, 4)],
names=['macd_fast_window', 'macd_slow_window', 'macd_signal_window', 'split_idx'])

Анализ производительности данных в выборке

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

# Get stats of the first best combination
print(in_pf[(13, 31, 10, 0)].stats())

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

# Get Trades
display(in_pf[(13, 31, 10, 0)].trades.records)

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

Наконец, мы можем отобразить сделки, совершенные портфелем, на ценовом графике:

# Plot Trades
in_pf[(13, 31, 10, 0)].trades.plot().show_svg()

Приведенный выше код создает график, показывающий точки входа и выхода из сделок.

Коэффициент Шарпа со стратегией «Покупай и держи» для каждого разделения данных

Прежде чем мы сможем оценить коэффициент Шарпа вне выборки нашей стратегии MACD, нам нужен эталон для сравнения. Для этого мы можем смоделировать торговую стратегию «купи и держи» на каждом сплите данных и рассчитать коэффициент Шарпа для каждого эталона.

# Simulate buy-and-hold function
def simulate_holding(price, **kwargs):
pf = vbt.Portfolio.from_holding(price, **kwargs)
return pf.sharpe_ratio()

# In-sample Sharpe Ratio with buy-and-hold
in_hold_sharpe = simulate_holding(in_price, **pf_kwargs)

# Out-sample Sharpe Ratio with buy-and-hold
out_hold_sharpe = simulate_holding(out_price, **pf_kwargs)

Этот код сначала определяет функцию simulate_holding, которая создает сигнал портфеля, который удерживает актив в течение всего периода, а затем вычисляет коэффициент Шарпа, используя результирующий портфель. Мы вызываем эту функцию как для in_price, так и для out_price.

Моделирование всех параметров MACD для диапазонов вне выборки

Мы используем эту же функцию simulate_all_params_MACD для моделирования производительности MACD в диапазоне вне выборки с использованием всех возможных комбинаций гиперпараметров.

# Simulate all MACD params for out-sample ranges
out_pf = simulate_all_params_MACD(out_price, fast_windows, slow_windows, signal_windows, **pf_kwargs)

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

Моделирование наилучших параметров MACD для диапазонов вне выборки

После нахождения оптимальных гиперпараметров для данных в выборке мы используем их для моделирования производительности MACD для диапазона вне выборки. Однако, поскольку существует только одна комбинация параметров для каждого разделения, мы не включаем график 3D-объема, так как он не предоставит никакой дополнительной информации.

def simulate_best_params_MACD(price, in_best_fast_windows, in_best_slow_windows, in_best_signal_windows, **kwargs):
# Run MACD indicator - per_column=True for one combination per column.
macd_ind = vbt.MACD.run(price, fast_window=in_best_fast_windows, slow_window=in_best_slow_windows, signal_window=in_best_signal_windows, per_column=True)

# Long when MACD is above zero AND signal
entries = macd_ind.macd_above(0) & macd_ind.macd_above(macd_ind.signal)

# Short when MACD is below zero OR signal
exits = macd_ind.macd_below(0) | macd_ind.macd_below(macd_ind.signal)

# Build portfolio
pf = vbt.Portfolio.from_signals(price, entries, exits, **kwargs)

return pf

# Use best params from in-sample ranges and simulate them for out-sample ranges
out_test_pf = simulate_best_params_MACD(out_price, in_best_fast_windows, in_best_slow_windows, in_best_signal_windows, **pf_kwargs)
print(out_test_pf.sharpe_ratio())

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

Анализ производительности вневыборочных данных

Мы можем вызвать out_test_pf.stats() для вывода статистики последней комбинации гиперпараметров (самых последних данных), которая является оптимальной комбинацией, найденной с использованием данных в выборке.

# Get stats of the last best combination
print(out_test_pf[(33, 35, 20, 19)].stats())

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

# Get Trades
display(out_test_pf[(33, 35, 20, 19)].trades.records)

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

# Get Trades
out_test_pf[(33, 35, 20, 19)].trades.plot().show_svg()

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

Сравнение стратегий buy-and-hold и MACD

Наконец, мы сравнили эффективность стратегии MACD с простым подходом «купи и держи» с использованием cv_results_df фрейма данных.

# In-sample and Out-sample results DF
cv_results_df = pd.DataFrame({
'in_sample_hold': in_hold_sharpe.values,
'in_sample_median': in_pf.sharpe_ratio().groupby('split_idx').median().values,
'in_sample_best': in_pf.sharpe_ratio()[in_best_index].values,
'out_sample_hold': out_hold_sharpe.values,
'out_sample_median': out_pf.sharpe_ratio().groupby('split_idx').median().values,
'out_sample_test': out_test_pf.sharpe_ratio().values
})

DataFrame сравнивает коэффициенты Шарпа, полученные с помощью стратегии «купи и держи», с коэффициентами Шарпа, полученными с помощью стратегии MACD. Мы рассчитали коэффициент Шарпа для каждого разделения данных в выборке и вне выборки, а затем вычислили три типа коэффициентов Шарпа:

  • Коэффициент Шарпа с использованием Buy-and-Hold: Это коэффициент Шарпа простой стратегии покупки и удержания для каждого сплита.
  • Медианный коэффициент Шарпа: Это медианный коэффициент Шарпа по всем комбинациям гиперпараметров.
  • Лучший коэффициент Шарпа: Это коэффициент Шарпа для наилучшей комбинации гиперпараметров.

Ниже приведены пять последних строк этого DataFrame:

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

color_schema = vbt.settings['plotting']['color_schema']

cv_results_df.vbt.plot(
trace_kwargs=[
dict(line_color=color_schema['blue']),
dict(line_color=color_schema['blue'], line_dash='dash'),
dict(line_color=color_schema['blue'], line_dash='dot'),
dict(line_color=color_schema['orange']),
dict(line_color=color_schema['orange'], line_dash='dash'),
dict(line_color=color_schema['orange'], line_dash='dot')
]
).show_svg()

Сюжет состоит из шести линий:

  • Синяя сплошная линия: медианный коэффициент Шарпа или MACD для данных в выборке.
  • Синяя пунктирная линия: наилучшая комбинация гиперпараметров или MACD для данных в выборке.
  • Синяя пунктирная линия: коэффициент Шарпа с использованием Buy-and-Hold для данных в выборке.
  • Оранжевая сплошная линия: медианный коэффициент Шарпа или MACD для данных вне выборки.
  • Оранжевая пунктирная линия: лучшая комбинация гиперпараметров или MACD для данных вне выборки.
  • Оранжевая пунктирная линия: коэффициент Шарпа с использованием Buy-and-Hold для данных вне выборки.

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

Заключение

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

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

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

Источник