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

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

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

Vectorbt – это высокопроизводительная платформа для тестирования на истории, состоящая из масштабируемых, компонуемых и расширяемых инструментов векторизованного тестирования и анализа на основе pandas. Это позволяет вам использовать 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
)

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

# 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 быстрых x 49 медленных 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_window, slow_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

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

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

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

Как только мы определили нашу функцию 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 для диапазона вне выборки. Однако, поскольку для каждого разделения используется только одна комбинация параметров, мы не включаем трехмерный объемный график, поскольку он не предоставляет никакой дополнительной информации.

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()

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

Сравнение стратегий «купи и держи» и 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. Мы рассчитали коэффициент Шарпа для каждого разделения данных внутри и вне выборки, а затем вычислили три типа коэффициентов Шарпа:

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

Ниже приведены пять последних строк этого 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 для данных в выборке.
  • Синяя пунктирная линия: коэффициент Шарпа с использованием модели «покупай и держи» для данных в выборке.
  • Оранжевая сплошная линия: медианный коэффициент Шарпа или MACD для данных вне выборки.
  • Оранжевая пунктирная линия: лучшая комбинация гиперпараметров или MACD для данных вне выборки.
  • Оранжевая пунктирная линия: коэффициент Шарпа с использованием модели «покупай и держи» для данных вне выборки.

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

Заключение

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

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

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

Станьте участником Medium сегодня и получите неограниченный доступ к тысячам руководств по Python и статьям по науке о данных! Всего за 5 долларов в месяц вы получите доступ к эксклюзивному контенту и поддержите меня как писателя. Зарегистрируйтесь сейчас, используя мою ссылку, и я получу небольшую комиссию без каких-либо дополнительных затрат для вас.

Подпишитесь на DDIntel Здесь.

Посетите наш сайт здесь: https://www.datadriveninvestor.com

Присоединяйтесь к нашей сети здесь: https://datadriveninvestor.com/collaborate