Индикатор MACD широко известен среди трейдеров💵. Мне его сигналы помогают находить развороты⤴ и предупреждения о коррекциях⛔. Много написано, как использовать его сигналы для открытия позиций, а мы сегодня рассмотрим прикладное применение в алготрейдинге🤖.
Все будет тестироваться на Quantopian (см. сюда), писать код будем на Python🐍. Рассмотрим следующие стратегии:
- Что надо знать и как не надо делать.
- Как есть: гистограмма, линия MACD, сигнальная.
- Добавим стоп-лосс.
- Торгуем в двух направлениях.
- Отфильтруем боковики и волатильность.
Пара слов о MACD
Индикатор основан на экспоненциальных скользящих средних и показывает их пересечения. Когда основная линия MACD над нулем, это означает, что быстрая средняя находится над медленной, а цена растет в краткосрочном периоде. Положение гистограммы над нулем говорит о росте цены. Дополнительно на индикаторе ищут расхождения движения цены и сигнальных линий, что может предупредить о развороте тенденции, но это уже другая история.
Базовые параметры:
- быстрая скользящая — EMA(12)
- медленная скользащая — EMA(26)
- сигнальная линия — EMA(9)
Разница первых двух используется для построения линии MACD. Сигнальная линия сглаживает линию MACD. Разница сигнальной и линии MACD даёт гистограмму.
🎓Гипотеза
Исходя из теории и графика, предположим, что при пересечении любой составляющей индикатора MACD нуля снизу вверх сообщает нам, что цена начала расти. Гистограмма дает сигнал раньше всех и обладает большим количеством ложных сигналов, линия MACD дает меньше сигналов, а самая чистая должна быть сигнальная линия.
Исходя из этого, мы должны входить в рынок только на росте и будем брать все растущие движения. Звучит отлично!👍
Условия тестирования:
- SPY
- c 01/01/2004 до 30/12/2016 (13 лет)
- торгуем спустя 1 час после открытия рынка
☔Что надо знать и как не надо делать
Начнем с простого теста, попробуем покупать актив по факту пересечения нуля снизу вверх каждой составляющей индикатора MACD: гистограмма, линия, сигнальная линия. Каждый день будем брать 40 дней истории и рассчитывать значения индикатора с помощью библиотеки ta-lib. Внизу графики тестов:
Данные оказались обратными предположению, гистограмма дает наилучший результат, затем идет линия, а хуже всех показатели у сигнальной. Пристальное изучение позволило найти ошибку, все дело в нестабильном периоде экспоненциальной средней (EMA). Ее значения напрямую зависят от длины анализируемой истории. Если история короткая, то библиотека ta-lib рассчитывает ее равной обычной скользящей средней (SMA). А это дает удивительно большую ошибку, так как сам MACD весь состоит из EMA.
Увеличив период истории сразу до 500 дней, исключим любой намек на подобные ошибки и получим результаты, которые и предположили. Далее рассмотрим стратегию «Как есть».
👎Как есть: гистограмма, линия MACD, сигнальная
Простой подход, чтобы посмотреть, как алгоритм работает. Какие результаты будут для каждого сигнала и, может быть, нас это наведет на какие-то интересные мысли. Сводная таблица результатов доступна в конце статьи. Графики результатов (правильных на этот раз) доступны ниже. Гипотеза подтвердилась:
- Гистограмма дает много сигналов, но среди них много ложных.
- Линия MACD уже чище и показывает лучшие результаты.
- Сигнальная линия дает наибольшую прибыль и наименьшую просадку.
Код алгоритма:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import talib # настраиваем переменные def initialize(context): # тестируем только на SPY context.stocks = symbols('SPY') # 'DIA', 'SPY', 'QQQ', 'IWM' context.pct_per_stock = 1.0 / len(context.stocks) # вызываем раз в день, спустя 1 час после открытия schedule_function(only_macd_long, date_rules.every_day(), time_rules.market_open(hours=1)) # ежедневный вызов def only_macd_long(context, data): # история для рассчета индикаторов (мин. в 2 раза больше нужной EMA) prices = data.history(context.stocks, 'price', 500, '1d') # обрабатываем список акций for stock in context.stocks: # получаем индикаторы MACD macd_line, signal, hist = talib.MACD(prices[stock], fastperiod=12, slowperiod=26, signalperiod=9) # используем последнее значение гистограммы MACD macd = hist[-1] current_position = context.portfolio.positions[stock].amount # закрываем имеющуюся позицию, если сигнал под нулем if macd < 0 and current_position > 0 and data.can_trade(stock): order_target(stock, 0) # открываем позицию, если сигнал над нулем и портфель пуст elif macd > 0 and current_position == 0 and data.can_trade(stock): order_target_percent(stock, context.pct_per_stock) |
👎Добавим стоп-лосс
Так как мы торгуем только в лонг и у нас есть просадка в -20%, попробуем добавить стопы в 3% от цены открытия. До этого мы закрывались при пересечении сигнальной линией MACD и нуля. Теперь будем контролировать просадку и закрываться за час до конца торгов, если цена опустилась ниже 3% порога.
На графике ниже видны не сногсшибательные результаты. Удалось отыграть ~1% просадки и поплатиться частью прибыли. Дополнительно поставлен фильтр, чтобы алгоритм не перезаходил на падающей цене — цена предыдущего дня должна быть ниже текущей.
Код алгоритма ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
import talib # настраиваем переменные def initialize(context): # тестируем только на SPY context.stocks = symbols('SPY') # 'DIA', 'SPY', 'QQQ', 'IWM' context.pct_per_stock = 1.0 / len(context.stocks) # вызываем раз в день, спустя 1 час после открытия schedule_function(only_macd_long, date_rules.every_day(), time_rules.market_open(hours=1)) context.stoploss = {s: 0 for s in context.stocks} context.sl = 0.03 # вызываем раз в день, за 1 час до закрытия, чтобы закрыть позицию по стопу schedule_function(check_to_stop, date_rules.every_day(), time_rules.market_close(hours=1)) # ежедневная проверка сигналов def only_macd_long(context, data): # история для рассчета индикаторов (мин. в 2 раза больше нужной EMA) prices = data.history(context.stocks, 'price', 500, '1d') # обрабатываем список акций for stock in context.stocks: # пропускаем, если нельзя торговать if not data.can_trade(stock): continue # получаем индикаторы MACD macd_line, signal, hist = talib.MACD(prices[stock], fastperiod=12, slowperiod=26, signalperiod=9) # используем последнее значение гистограммы MACD macd = signal[-1] current_position = context.portfolio.positions[stock].amount # закрываем имеющуюся позицию, если сигнал под нулем if macd < 0 and current_position > 0: order_target(stock, 0) # открываем позицию, если сигнал над нулем и портфель пуст elif macd > 0 and current_position == 0 and prices[stock][-2] < prices[stock][-1]: order_target_percent(stock, context.pct_per_stock) # ставим уровень стопа context.stoploss[stock] = prices[stock][-1] * (1 - context.sl) # ежедневная проверка на стопы за 1 час до закрытия def check_to_stop(context, data): # получение цен prices = data.current(context.stocks, 'price') # обрабатываем список акций for stock in context.stocks: # пропускаем, если нельзя торговать if not data.can_trade(stock): continue current_position = context.portfolio.positions[stock].amount # пододвигаем стоп sl = prices[stock] * (1 - context.sl) if context.stoploss[stock] < sl: context.stoploss[stock] = sl else: sl = context.stoploss[stock] # ставим стоп-ордер, если есть позиция if prices[stock] < sl and current_position > 0: order_target(stock, 0) |
👎Торгуем в двух направлениях
Раз не получилось выжать из алгоритма со стопами, попробуем торговать в обоих направлениях. Предыдущие опыты подсказывают, что хорошего может ничего не получиться, но надо проверять.
Удалось заметить, что основные проблемы случаются в моменты боковиков или резких подъемов и падений за короткие промежутки времени. Избавиться от этих проблем можно фильтрацией волатильности и боковиков.
Код торговли в двух направлениях не публикую, там минимум изменений. Если нужно, пишите в комментариях, вышлю на почту.
👎Отфильтруем волатильность и боковики
Волатильность будем фильтровать повышенным значением ATR относительно среднего за последние 200 дней. Боковики постараемся найти положением средних, когда SMA20 находится в 1,5% от SMA50. В это время позиции не открываем.
Результаты показывают, что купи-держи значительно лучше, а вот просадку удается сократить до 13%. Хоть что-то…
Код алгоритма ниже:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
import talib import numpy as np # настраиваем переменные def initialize(context): # тестируем только на SPY context.stocks = symbols('SPY') # 'DIA', 'SPY', 'QQQ', 'IWM' context.pct_per_stock = 1.0 / len(context.stocks) # вызываем раз в день, спустя 1 час после открытия schedule_function(macd_filter, date_rules.every_day(), time_rules.market_open(hours=1)) # ежедневная проверка сигналов def macd_filter(context, data): # история для рассчета индикаторов (мин. в 2 раза больше нужной EMA) prices = data.history(context.stocks, ['price', 'high', 'low'], 500, '1d') # обрабатываем список акций for stock in context.stocks: # пропускаем, если нельзя торговать if not data.can_trade(stock): continue # получаем ATR atr = talib.ATR(prices['high'][stock], prices['low'][stock], prices['price'][stock], timeperiod=14) # получаем среднее значение, чтобы фильтровать mean_atr = talib.SMA(atr, timeperiod=200)[-1] # получаем средние для определения расстояния между MA ma_short = talib.SMA(prices['price'][stock], timeperiod=20) ma_long = talib.SMA(prices['price'][stock], timeperiod=50) ma_diff = abs(ma_short - ma_long) / ma_long #record('atr', atr[-1], 'mean_atr', mean_atr, 'ma_diff', ma_diff[-1]) # получаем индикаторы MACD macd_line, signal, hist = talib.MACD(prices['price'][stock], fastperiod=12, slowperiod=26, signalperiod=9) # используем последнее значение гистограммы MACD macd = signal[-1] current_position = context.portfolio.positions[stock].amount # закрываем имеющуюся позицию, если сигнал под нулем if macd < 0 and current_position > 0: order_target(stock, 0) # открываем позицию, если сигнал над нулем и портфель пуст elif macd > 0 and current_position == 0 and signal[-2] < signal[-1]: if atr[-1] <= mean_atr and ma_diff[-1] > 0.015: order_target_percent(stock, context.pct_per_stock) |
🥇Результаты
Стратегия | Время торгов | Кол-во сделок | PnL | Доходность | Просадки |
---|---|---|---|---|---|
Купи-Держи | Open | 53/0 | - | 160.1% | -54.9% |
Как есть: гистограмма | Open+1 | 163/163 | 13% | 30.8% | -29.3% |
Как есть: линия MACD | Open+1 | 69/68 | 19% | 68% | -22.3% |
Как есть: сигнальная линия | Open+1 | 40/39 | 19% | 90.2% | -21.6% |
Стоп-лосс | Open+1 | 92/91 | 18% | 77% | -19% |
В обе стороны | Open+1 | 40/39 | 6% | 28% | -27% |
Фильтр | Open+1 | 24/23 | 19% | 49.6% | -12.9% |
- Время торгов — время, когда алгоритм начинает торговать: Open — открытие рынка, Open+1 — через 1 час после открытия рынка.
- Кол-во сделок — количество сделок на покупку и продажу. В поле указаны Покупки/Продажи.
- PnL — отношение прибыльных дней к убыточным. Если прибыльных больше — значение положительное.
🏁Заключение
Результаты тестов показывают, что в голом виде индикатор MACD не подходит для создания несметного богатства💰, во всяком случае на SPY. Удивительно, но обнаружил относительно хорошее поведение MACD и QQQ (здесь не публикую, попробуйте сами), в сравнении с другими «голыми» индикаторами.
Можно продолжать поиск решений как отфильтровать боковики или пилу. Можно попробовать подбирать разные настройки индикатора для разных активов в разные периоды. А можно его отложить до лучших времен и перейти к тестированию импульсной стратегии, основанной на ATR, которую я рассмотрю на следующей неделе.
💬Напишите в комментариях, как можно улучшить алгоритм с MACD. Где корень неудач? Или предложите, какие стратегии стоит рассмотреть в будущем.
Автор Quantrum.me
Telegram-канал📣: @quantiki
Подбор и тестирование портфелей. Подключение стратегий к IB.