Первый из трех способов автоматического поиска пар🎏 на Python🐍 для торговли по стратегии «Парного трейдинга». Исходя из результатов предыдущей статьи, во всех примерах мы будем использовать только поиск коинтеграции.
Кратко о «Парном трейдинге»: в основе стратегии лежит предположение, что есть две акции, которые имеют глубокую экономическую связь друг с другом, и их цена движется в одном направлении с разной скоростью. Когда отстает акция А, мы ее покупаем и одновременно продаем в короткую акцию Б. И наоборот.
Используем дневные цены закрытия, отрегулированные на дивиденды и сплиты. Вы можете скачать бесплатную историю дневных цен с Quandl.
📏Подготовка к поиску
Для правильной работы необходимо соблюсти следующие условия:
- Истории цен должны быть равной длины. (Результат сравнения историй за 200 дней и за 20 дней может быть непредсказуем).
- Значения должны быть переведены в относительные величины. (Тяжело сравнивать активы за $200 и за $1).
- Каждая история не должна обладать стационарностью сама по себе. (Важна именно стационарность спреда пары, а не отдельного актива в ней).
Коинтеграцию каждого времянного ряда будем проверять с помощью метода Дики-Фуллера из библиотеки statsmodels. Код подготовки ниже:
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 |
def get_performance(a): """ Конвертируем историю в относительные величины """ return np.insert(np.cumsum(np.diff(a) / a[:-1] * 100.), 0, 0) # получаем все длины историй цен all_lengths = [len(p) for s, p in symbol_prices.items()] # получаем порог максимальной цены для 5% минимальных историй length_limit = int(np.percentile(all_lengths, 5)) # фильтруем короткие истории, обрезаем длинные, переводим в относительные величины symbol_performance = {s: get_performance(p[-length_limit:]) for s, p in symbol_prices.items() if len(p) >= length_limit} # проверяем коинтеграцию и удаляем стационарные ряды to_delete = [] for s, p in symbol_performance.items(): result = adfuller(p) score = result[0] pvalue = result[1] crit = result[4] #score, pvalue, score < crit['5%'] if score < crit['5%']: # cointegrated to_delete.append(s) if to_delete: for s in to_delete: del symbol_performance[s] |
☝Выбор акций для поиска
Искать пары будем среди активов торгующихся на рынке, включая ETF, ограничив следующими условиями:
- Цена более $10.
- Средний объем более 500 тыс. акций в день.
- ATR за 13 дней более $0.40.
Проверяем на ликвидных активах с хорошим движением внутри дня. Из 6 тысяч американских акций, торгующихся на биржах, нам подходят ~1500 активов. Тестировать будем предыдущий год (360 календарных дней).
🔍1 из 3: Простой коинтеграционный тест
Использую самую первую попавшуюся на глаза функцию для проверки пары на стационарность. Это тест на наличие единичных корней найден в библиотеке statsmodels.
Функция проверки стационарности:
statsmodels.tsa.stattools.coint(X, Y)
Функция поиска пар получает словарь с историей относительного изменения цен и последовательно проверяет каждую пару. Для 1500 активов это примерно 1.1 миллиона вариантов.
Выбираем пары с оценкой ниже 5% уровня ошибки и p-значением меньше 0,001. Код поиска пар ниже:
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 |
def find_cointegrated_pairs(symbol_prices, need_preparation=False): # готовим матрицы для сбора оценок и p-значений n = len(symbol_prices) score_matrix = np.zeros((n, n)) pvalue_matrix = np.ones((n, n)) 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_pair(S1, S2, to_performance=True) # проверяем коинтеграцию # crit[0] - 1%; crit[1] - 5%; crit[2] - 10% score, pvalue, crit = coint(S1, S2) # заполняем матрицы значений score_matrix[i, j] = score pvalue_matrix[i, j] = pvalue # добавляем пары, если score < crit[5%] и p-значение < 0.001 if score < crit[1] and pvalue < 0.001: pairs.append((symbols[i], symbols[j], pvalue)) # сортируем пары по возрастанию p-значения sorted_pairs = sorted(pairs, key=operator.itemgetter(2)) return score_matrix, pvalue_matrix, sorted_pairs |
На момент написания статьи тест нашел 6400 пар (~0.6%), поиск загружал 1 ядро процессора и длился порядка 45 минут. Вот первые несколько пар с наименьшим p-значением:
- XIV, SVXY
- IWB, SPY
- IVV, SPY
- DUST, GDX
- SPY, SPXU
- …
Видно, что это ETF на индексы и первые два действительно имеют стационарный спред, а вот начиная с IVV и SPY найти проблемы нам помогут графики.
🎏Проверка найденных пар
Проверим несколько графиков найденных пар и оценим результаты, чтобы оценить, можно ли положиться на скрипт для автоматического поиска. Для проверки возьмем произвольные пары:
- XIV, SVXY
- DUST, GDX
- XLK, QQQ
- PRU, C
- TMUS, WCG
👎XIV, SVXY
XIV — ETN (аналог ETF) обратный индексу страха $VIX.
VSXY — ETF обратный индексу страха $VIX.
На графике история цен и сигнальная линия z-оценки. SVXY домножен на коэффициент для приведения цен к единой величине. Видно, что график хорош, но два месяца назад (ноябрь-декабрь 2016) спред уменьшился. Причина в моем поставщике данных. Ранее была бо́льшая погрешность в цене, которую нивелировали.
Код z-оценки:
1 2 |
def zscore_std(series): return ((series - series.mean()) / np.std(series)).rename("z-score") |
👎DUST, GDX
DUST — 3х-кратный обратный ETF к GDX.
GDX — ETF компаний-золотодобытчиков.
График разочаровывает и подобных графиков в результатах много. Данные активы обратно-скоррелированы — этот факт в их природе существования. Функция coint(X, Y) на вход получает два времянных ряда и сама проверяет их стационарность. Видимо эта функция использует в основе проверку корреляции и дополнительно подгоняет ряды.
👍XLK, QQQ
XLK — ETF на индекс технологических компаний.
QQQ — ETF на индекс NASDAQ-100, где преимущественно технологические компании.
Здесь результаты лучше, но так как в QQQ также много компаний из XLV (здравоохранение), то в январе 2017 XLK сумел вырваться вперед.
👌PRU, C
Копнем поглубже и возьмем несколько компаний.
PRU — Prudential Financials, страховая компания.
C — Citibank, банк.
Большую часть года компании шли в ногу, что не удивительно, так как обе в XLF (финансовый сектор) и обе выигрывают от роста ставок. Но за январь 2017 появилось расхождение.
👍TMUS, WCG
TMUS — T-Mobile US, американский сотовый оператор.
WCG — WellCare Health Plans, компания из сектора здравоохранения.
Связь данной пары меня удивляет, но с графиком не поспоришь, они идут нога в ногу и обладают хорошим спредом.
🏁Вывод
Данный метод имеет право на существование. Удается найти хорошие пары, которые достойны участия в бэктестинге. Но одновременно к нам приходит большое количество шлака💩, что заставляет отсматривать👀 результаты вручную. Лучше себя проявил поиск с помощью метода Дики-Фуллера, о котором я буду писать в следующей статье.
💬В комментариях задавайте вопросы и спрашивайте, что описать подробнее. Покажите примеры лучших пар, по вашему мнению.
Автор Quantrum.me
Telegram-канал📣: @quantiki
Подбор и тестирование портфелей. Подключение стратегий к IB.