Простой бэк-тест на Python по комбинации нескольких технических элементов.

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

Я только что опубликовал новую книгу после успеха Новые технические индикаторы в Python. Он содержит более полное описание и добавление сложных торговых стратегий со страницей Github, посвященной постоянно обновляемому коду. Если вы считаете, что это вас заинтересует, не стесняйтесь перейти по приведенной ниже ссылке или если вы предпочитаете купить версию в формате PDF, вы можете связаться со мной в Linkedin.



В рамках технического анализа мы можем найти два метода, которые можно легко объединить вместе:

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

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



Паттерн доджи

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

Логика кодирования доджи чрезвычайно проста. Мы просто напишем условие, которое находит бары (свечи), где цена открытия равна цене закрытия. Поскольку пары FX указываются с точностью до 5 знаков после запятой, такое может случиться очень редко, поэтому мы округлим числа до 4 знаков после запятой.

Без индикатора функции будет сложно понять, должна ли она быть бычьей доджи или медвежьей. Можно с уверенностью сказать, что мы ищем медвежьи доджи, когда индикатор находится на уровнях перекупленности, и ищем бычьи дожи, когда индикатор находится на уровнях перепроданности. У нас не будет этой проблемы позже, когда мы включим RSI в бэк-тест, но в случае, если нас интересует чистый сканер Доджи, мы можем добавить небольшое условие с целью убедиться, что предыдущая свеча (или две) определенного типа. Как это будет выглядеть? Эта функция показывает, как сканировать доджи в наборе данных OHLC:

def doji_scan(Data, buy, sell):
for i in range(len(Data)):
        
 if Data[i - 1, 3] < Data[i - 1, 0 ] and Data[i, 3] == Data[i, 0]:
  Data[i, buy] = 1
        
 if Data[i - 1, 3] > Data[i - 1, 0 ] and Data[i, 3] == Data[i, 0]:
  Data[i, sell] = -1
# The variable Data refers to the OHLC Historical Data array.
# The variable buy refers to where the long order is triggered
# The variable sell refers to where the short order is triggered

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

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

Data = np.round(Data, 4)

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



Итак, давайте теперь создадим следующие условия для нашей сложной торговой стратегии:

  • Покупайте (открывайте длинную позицию) всякий раз, когда рынок печатает паттерн Доджи, одновременно показывая значение RSI меньше 30.
  • Продавайте (открывайте короткую позицию) всякий раз, когда рынок печатает паттерн Доджи, одновременно показывая значение RSI больше 70.
def complex_strategy_1(Data, buy, sell):
 for i in range(len(Data)):
        
  if Data[i, indicator] < lower and Data[i, 3] == Data[i, 0]:
   Data[i, buy] = 1
        
  if Data[i, indicator] > upper and Data[i, 3] == Data[i, 0]:
   Data[i, sell] = -1
# The variable Data refers to the OHLC Historical Data array.
# The variable indicator refers to RSI's location.
# The variable buy refers to where the long order is triggered.
# The variable sell refers to where the short order is triggered.

Управление рисками будет следовать 4x ATR для стопа и 1x ATR для цели. Опять же, это неоптимальный метод управления рисками, но с систематическими стратегиями я предпочитаю оставлять передышку для стратегий и больше играть на коэффициенте попаданий, поскольку я ожидаю реакции, а не установленных тенденций. Я не делаю этого с помощью дискреционных стратегий. Кроме того, мы ограничим тестирование на истории парами EURUSD и USDCHF, чтобы не потеряться слишком много информации. Результаты показаны ниже:

Образец сигнальной диаграммы можно увидеть ниже:

И, наконец, кривая капитала для двух валютных пар после 0,1 пипса за действие (сделка будет стоить 0,2 пипса). Правила выхода устанавливаются ATR или запуском другого сигнала:

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



Образец Харами

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

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

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

def harami_scan(Data, buy, sell):
 for i in range(len(Data)):
        
  if Data[i - 1, 3] < Data[i - 1, 0] and \
     Data[i, 3] > Data[i, 0] and \
     Data[i - 1, 3] < Data[i, 2] and \
     Data[i - 1, 0] > Data[i, 1]:
      Data[i, buy] = 1
        
  if Data[i - 1, 3] > Data[i - 1, 0] and \
     Data[i, 3] < Data[i, 0] and \
     Data[i - 1, 3] > Data[i, 1] and \
     Data[i - 1, 0] < Data[i, 2]:
      Data[i, sell] = -1
# The variable Data refers to the OHLC Historical Data array.
# The variable buy refers to where the long order is triggered
# The variable sell refers to where the short order is triggered

Если вам интересно понять, что на самом деле делает приведенная выше функция, то вот она, сначала с бычьим Харами, мы хотим, чтобы текущий бар был бычьим, и поэтому мы должны указать условие, при котором цена закрытия Data [i , 3] должна быть больше, чем цена открытия Data [i, 0]. Затем мы хотим убедиться, что предыдущий бар медвежий, то есть это красная свеча. Итак, мы убеждаемся, что Data [i - 1, 3] меньше, чем Data [i - 1, 0]. И, наконец, мы должны убедиться, что предыдущее закрытие меньше текущего минимума, таким образом показывая, что Data [i -1, 3] меньше, чем Data [i, 2] , и в то же время предыдущее открытие должно быть больше, чем текущий максимум, что переводится в Data [i - 1,0], которое больше, чем Data [i, 1 ]. Противоположное рассуждение касается медвежьего Харами.

def complex_strategy_2(Data, indicator, buy, sell):
for i in range(len(Data)):
        
  if Data[i, indicator] < lower and \
     Data[i - 1, 3] < Data[i - 1, 0] and \
     Data[i, 3] > Data[i, 0] and \
     Data[i - 1, 3] < Data[i, 2] and \
     Data[i - 1, 0] > Data[i, 1]:
      Data[i, buy] = 1
        
  if Data[i, indicator] > upper and \
     Data[i - 1, 3] > Data[i - 1, 0] and \
     Data[i, 3] < Data[i, 0] and \
     Data[i - 1, 3] > Data[i, 1] and \
     Data[i - 1, 0] < Data[i, 2]:
      Data[i, sell] = -1
# The variable Data refers to the OHLC Historical Data array.
# The variable indicator refers to RSI's location.
# The variable buy refers to where the long order is triggered.
# The variable sell refers to where the short order is triggered.

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

  • Покупайте (открывайте длинную позицию) всякий раз, когда рынок выявляет бычий паттерн Харами, одновременно показывая значение RSI меньше 45.
  • Продавайте (открывайте короткую позицию) всякий раз, когда рынок выявляет медвежий паттерн Харами, одновременно показывая значение RSI больше 55.
# Defining the barriers
lower = 45
upper = 55

Результаты показаны ниже:

Образец сигнальной диаграммы можно увидеть ниже:

И, наконец, кривая капитала по двум валютным парам:

Паттерн пронзительный и темное облако

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

Узор из темных облаков

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

Узор пирсинга

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

def piercing_scan(Data, buy, sell):
    for i in range(len(Data)):
        
        if  Data[i - 1, 3] < Data[i - 1, 0] and \
            Data[i, 3] > Data[i, 0] and \
            Data[i, 0] < Data[i - 1, 3] and \
            Data[i, 3] > Data[i - 1, 3] and \
            Data[i, 3] < Data[i - 1, 0]:
            Data[i, buy] = 1
def cloud_scan(Data, buy, sell):
        if  Data[i - 1, 3] > Data[i - 1, 0] and \
            Data[i, 3] < Data[i, 0] and \
            Data[i, 0] > Data[i - 1, 3] and \
            Data[i, 3] < Data[i - 1, 3] and \
            Data[i, 3] > Data[i - 1, 0]:
            Data[i, sell] = -1
# Normally, we should put them together inside one function.
# The variable Data refers to the OHLC Historical Data array.
# The variable buy refers to where the long order is triggered
# The variable sell refers to where the short order is triggered

Итак, давайте теперь создадим следующие условия для нашей сложной торговой стратегии:

  • Покупайте (открывайте длинную позицию) всякий раз, когда рынок печатает пронзительный паттерн, одновременно показывая значение RSI меньше 40.
  • Продавайте (открывайте короткую позицию) всякий раз, когда рынок печатает паттерн темного облака, одновременно показывая значение RSI больше 60.
def complex_strategy_3(Data, buy, sell):
for i in range(len(Data)):
        
        if Data[i, indicator] < lower and \
            Data[i - 1, 3] < Data[i - 1, 0] and \
            Data[i, 3] > Data[i, 0] and \
            Data[i, 0] < Data[i - 1, 3] and \
            Data[i, 3] > Data[i - 1, 3] and \
            Data[i, 3] < Data[i - 1, 0]:
            Data[i, buy] = 1
        
        if Data[i, indicator] > upper and \
            Data[i - 1, 3] > Data[i - 1, 0] and \
            Data[i, 3] < Data[i, 0] and \
            Data[i, 0] > Data[i - 1, 3] and \
            Data[i, 3] < Data[i - 1, 3] and \
            Data[i, 3] > Data[i - 1, 0]:
            Data[i, sell] = -1
# The variable Data refers to the OHLC Historical Data array.
# The variable indicator refers to RSI's location.
# The variable buy refers to where the long order is triggered.
# The variable sell refers to where the short order is triggered.

Результаты показаны ниже:

Образец сигнальной диаграммы можно увидеть ниже:

И, наконец, кривая капитала по двум валютным парам:

Визуально сигналы также встречаются редко и лучше подходят для пары EURUSD, чем для пары USDCHF.

Заключение

Мы видим очевидную проблему с объединением индикаторов; редкость сигналов. Теперь с этой проблемой можно справиться с помощью других инструментов, но интуитивно понятно, что выравнивание планет происходит не так часто, и поэтому нам не следует ожидать одинаковой частоты сигналов, когда мы объединяем разные индикаторы с относительно длинными периодами ретроспективного анализа, такими как 14-периодный RSI по умолчанию. Однако интересно увидеть результаты на других рынках и в дальнейшем их оптимизировать.