Парный трейдинг: 3 из 3 способов поиска пар (EMA)

Это заключительная статья по автоматическому🤖 поиску пар🎏 для «Парного трейдинга» с помощью Python🐍. Способ самый быстрый🏎 и самый эффективный👍. Хотя эффективность достигается уже благодаря анализу полученного набора пар.

В основе данного способа лежит анализ скользящих средних каждого актива. Идея была взята здесь.

📽В предыдущих сериях

«Парный трейдинг» — стратегия торговли двух скоррелированных акций, двигающихся в одном направлении с разной скоростью. Надо одновременно купить отстающую и продать убежавшую. Рекомендовано к ознакомлению: Описание стратегии, Первый способ поиска пар, Второй способ поиска пар.

🎓Предположение

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

☝Выбор акций для поиска

  • Американский рынок.
  • История за предыдущие 360 календарных дней.
  • Цена более $10.
  • Средний объем более 500 тыс. акций в день.
  • ATR за 13 дней более $0.40.

На 10 февраля 2017 таких всего 1287 штук, минимальная длина истории 245. После фильтрации на отсутствие коинтеграции осталось 1028 штук, что дает нам 0,63 млн. пар. Подготовка историй цен подробно рассмотрена здесь.

🔍3 из 3: спред EMA-50

Попробуем сравнивать спред между EMA-50 (средние за 50 дней). Фильтр по порогу среднего значения спреда давал много шлака, что склонило меня к использованию 70% перцентиля. То есть нам подойдут пары, если максимальное абсолютное значение 70% спреда меньше трех. Три — это произвольное число и для себя можете попробовать использовать любое другое.

Дополнительно необходимо проверять найденные пары на стационарность. Вспоминая прошлые наблюдения, эта проверка является крайне прожорливой к ресурсам, но в этот раз на нашей стороне предварительный фильтр по спреду EMA-50 и нам необходимо проверить только найденные пары, коих будет не много.

Стационарность проверяем методом Дики-Фуллера отсюда:
statsmodels.tsa.stattools.adfuller(X)

По порядку:

  • EMA-50;
  • 70% перцентиль abs(спреда) меньше 3;
  • спред проверяем на стационарность.

Код на Python:

def find_pairs_sma(symbol_prices, need_preparation=False):
    n = len(symbol_prices)
    symbols = list(symbol_prices.keys())
    pairs = []
    
    for i in range(n):
        for j in range(i+1, n):
            S1 = symbol_prices[symbols[i]]
            S2 = symbol_prices[symbols[j]]
            
            # подготавливаем ряды, если необходимо
            if need_preparation:
                S1, S2 = prepare_vectors(S1, S2, to_performance=True)
                
            # получаем скользящие средние
            period = 50
            ema1 = talib.EMA(S1, timeperiod=period)
            ema2 = talib.EMA(S2, timeperiod=period)

            spread = (ema1[period-1:] - ema2[period-1:])
            # среднее значение спреда MA
            spread_mean = abs(spread).mean()
            # медиана спреда MA
            spread_median = np.median(abs(spread))
            # максимальное значение 70% значений по модулю спреда MA
            spread_percentile = np.percentile(abs(spread), 70)  
            
            # добавляем пары со средним значением спреда EMA50 < 3
            # ИЛИ медианой спреда EMA50 < 3
            # ИЛИ процентилем 70% от спреда EMA50 < 3
            if spread_percentile < 3:
                # проверяем стационарность
                result = adfuller(S1-S2)
                score, pvalue, crit = result[0], result[1], result[4]
                
                pairs.append((symbols[i], symbols[j], 
                              spread_median, spread_mean, spread_percentile, 
                              pvalue, score < crit['5%']))
                
    return pairs

На 10 февраля 2017 года метод нашел 2056 пар (~0.32%), включая не стационарные. Поиск загружал все ядра процессора, кроме одного и занял почти… 3 минуты, в отличие от второго способа, затянувшегося на 2 часа.

В этот раз, для исключения шлака, проведем сбор дополнительных данных и отфильтруем плохие пары. А искать будем следующее:

  • Стандартное отклонение спреда за последние 2 месяца.
  • Количество пересечений сигнальной z-оценкой -1, 0 и 1.

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

Код на Python:

results = []
for s1, s2, median, mean, percentile, pvalue, coint in pairs:
    res = {
        'Symbol1': s1,
        'Symbol2': s2,
        'Median': median,
        'Mean': mean,
        'Percentile': percentile,
        'pvalue': pvalue,
        'coint': coint
    }
    P1, P2 = symbol_performance[s1], symbol_performance[s2]
    spread = P1 - P2
    
    res['std'] = np.std(spread)
    res['std(-40)'] = np.std(spread[-40:])
    
    z = ((spread - spread.mean()) / np.std(spread))    
    res['x(0)'] = len(z[abs(np.insert(np.diff(np.sign(z)), 0, 0)) == 2])
    res['x(-1)'] = len(z[abs(np.insert(np.diff(np.sign(z+1)), 0, 0)) == 2]) 
    res['x(+1)'] = len(z[abs(np.insert(np.diff(np.sign(z-1)), 0, 0)) == 2])
    results.append(res)
    
d = pd.DataFrame(results)

# только стационарные
fltr = d['coint'] == True
# фильтр низкой сигмы за последние ~2 месяца
fltr = (fltr) & (d['std(-40)'] > 0.55)

print(len(d[fltr]))
d[fltr].sort_values(['x(0)'], ascending=False).head(10)

Вот пять пар с наибольшим количеством пересечений нуля z-оценкой:

  • SNV, KBWB
  • WEC, DUK
  • ETFC, SCHW
  • XLI, DIA
  • FULT, KBE

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

🎏Проверка найденных пар

В проверке будут участвовать следующие пары:

  • SNV, KBWB
  • ETFC, SCHW
  • XLI, DIA

👌SNV, KBWB

SNV — банк.
KBWB — ETF на банки.

Спред стационарен и дает сигналы для торговли.

👌ETFC, SCHW

ETFC — инвестиционный брокер.
SCHW — инвестиционный брокер.

Одна группа, стационарный спред, достаточное количество сигналов.

👌XLI, DIA

XLI — ETF на промышленный сектор.
DIA — ETF на промышленный индекс Доу Джонс.

Единая направленность фондов, достаточное количество сигналов и визуальное соответствие.

🏁Вывод

Анализ результатов поиска данным способом оставляет положительные впечатления. Все найденные пары, включая пары с минимальным количеством пересечений нуля сигнальной z-оценкой (мин. 8), удовлетворяют базовым требованиям стратегии «Парного трейдинга». Способ работает очень быстро и подходит для беглого обзора рынка.

В следующий раз вернемся к бэктестингу найденных пар на платформе Quantopian. Бэктестинг проведем, используя пары, найденные текущим способом.

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

🎓Обучение «Парному трейдингу» у профессионалов👍.
Александр Румянцев aka "iamraa"
Автор Quantrum.me
Интересуетесь алготрейдингом на Python? Присоединяйтесь к команде. Пишите в личку или на email.