Цель данной статьи — поделиться простейшей стратегией статистического арбитража, основанной на торговле коинтегрированными парами акций, которые были выявлены на Московской и Нью-Йоркской биржах.

Если мы возьмём пару коинтегрированных акций, то у нас есть возможность захеджироваться и построить рыночно-нейтральную стратегию, когда убытки по одной бумаге будут компенсироваться прибылями по другой. Как это выглядит на практике?

Торговая стратегия


Допустим, у нас есть коинтегрированная пара акций, $X$ и $Y$, а также цены этих акций за некий период времени $0,...,T$. Для примера возьмём пару акций с тикерами (VSYDP,NKHP) из моей предыдущей статьи про коинтеграцию. Для неё у нас есть данные о ценах за 215 торговых дней.

Первую половину наблюдений мы будем использовать, чтобы определить параметры торговой стратегии. Затем, основываясь на найденных параметрах, мы возьмём вторую половину наблюдений и проведём бэктесты, то есть протестируем, принесёт ли нам такая стратегия деньги.

Для дальнейших рассуждений мне понадобится спред двух акций, $s_t = y_t - \beta x_t$. В нашем примере с парой (VSYDP,NKHP) мы уже находили спред в предыдущей статье про коинтеграцию, так что здесь я просто продублирую картинку:

Итак, все мы хотим покупать дёшево и продавать дорого. Если спред уходит ниже нуля, акция $Y$ (VSYDP) дешевле, чем акция $X$ (NKHP). И, наоборот, если спред поднимается выше нуля, акция $Y$ дороже, чем акция $X$ (ну или $X$ дешевле по сравнению с $Y$).

Таким образом, по сути, торговая стратегия состоит в том, чтобы купить акцию $Y$ и продать акцию $X$ в соотношении $1 : \beta$, если спред находится несколько ниже нуля (ниже линии $G$ на рисунке). Когда же спред возвращается обратно к нулю, нам нужно закрыть позицию, продав $Y$ и купив $X$ в том же соотношении. В этом случае мы получаем прибыль размера $G$:

Здесь важно понимать, что у нас должна быть возможность продавать акции, которыми мы не владеем, — это называется короткой продажей (шорт). Также необходимо отметить, что не все брокеры включают возможность короткой продажи в стандартный пакет услуг, так что, возможно, вам придётся дополнительно обратиться к нему для расширения своих инвестиционных возможностей.

В общем, мы составляем портфель, который содержит одну длинную позицию (лонг) и одну короткую позицию (шорт), если спред пересекает некоторую линию $G$ или уходит за неё в противоположную от нуля сторону. И мы закрываем все позиции, когда спред возвращается обратно к нулю.

Следующий вопрос, который возникает, — это «как найти значение $G$»?

Как найти значение $G$


Помимо математики, я сразу буду приводить здесь реализацию в матлабе. Приведённый ниже код является естественным продолжением кода из предыдущей статьи про коинтеграцию. Нам понадобится первая половина наблюдений, $t = 0,...,T/2$, которую мы будем рассматривать как «историю».

T = length(testPrices);
half = round(T/2); 

Для начала найдём среднее отношение $Y$ и $X$ для первой половины наблюдений.

$\bar{r} = \frac{1}{T/2 + 1} \sum_{t=0}^{T/2}\frac{y_t}{x_t}.$


sumRatio = 0;
for i = 1 : half
    sumRatio = sumRatio + testPrices(i,1) / testPrices(i,2);
end
r = sumRatio / half;

Для пары акций с тикерами (VSYDP,NKHP) расчётное значение $\bar{r} = 34,3927$. Затем вычислим максимум абсолютного значения спреда для первой половины наблюдений:

$m = \max_t |y_t - \bar{r} x_t|, t=0,...,T/2.$


clear absspread
for i = 1 : half
    absspread(i,1) = abs(testPrices(i,1) - r * testPrices(i,2));
end
m = max(absspread);

Для пары акций с тикерами (VSYDP,NKHP) расчётное значение $m = 3204,4$. Теперь мы можем определить значение $G$ путём перебора: возьмём некоторый процент от $m$ и попробуем поторговать на «истории» при различных значениях этого процента, а затем выберем то значение, которое даст наибольшую прибыль. Это и будет искомое значение для линии $G$.

Перебор различных значений $G$ для поиска наилучшего


Для начала нам нужно посчитать количество трейдов. Первый трейд, обозначим его $t_1$, мы делаем, когда впервые встаём в позицию. В этом случае мы ещё не получаем прибыль:

$t_1 = \min t: |s_t| \geq g,$


где $s_t$ — спред. Далее успешными моментами трейдов будут:
  1. если $s_{t_n} \geq g$: $t_{n+1} = \min t: t > t_n, s_t \leq - g$;
  2. если $s_{t_n} \leq -g$: $t_{n+1} = \min t: t > t_n, s_t \geq g$.

Затем прибыль будет рассчитываться как $(d-1) \cdot 2g$, где $d$ — количество трейдов, $g$ — некоторый процент от $m$. Если трейдов не было, тогда прибыль равна нулю. $(d-1) \cdot 2g$ — это минимальная прибыль, если всегда торговать одним соотношением $Y$ и $\beta X$.

clear profit
for h = 1:10
    g = 0.05 * h * m;
    profit(h,1) = 0.05 * h * 100;
    profit(h,2) = g;
    clear trade
    k = 1;
    for i = 1:half - 1
        if abs(spread(i)) >= g
            trade(1,1) = i;
            trade(1,2) = spread(i);
            trade(1,3) = -1;
            trade(1,4) = beta;
            trade(1,5) = testPrices(i,1);
            trade(1,6) = testPrices(i,2);
            trade(1,7) = 0;
            startIndex = i;
            k = 2;
            break
        end
    end
    if k == 1
        break
    end
    for i = startIndex:half - 1
        if (trade(k-1,2) <= -g) && (spread(i) <= g) && (spread(i+1) >= g)
            trade(k,1) = i+1;
            trade(k,2) = spread(i+1);
            trade(k,3) = -1;
            trade(k,4) = beta;
            trade(k,5) = testPrices(i+1,1);
            trade(k,6) = testPrices(i+1,2);
            trade(k,7) = 0;
            k = k + 1;
        end
        if (trade(k-1,2) >= g) && (spread(i) > -g) && (spread(i+1) <= -g)
            trade(k,1) = i+1;
            trade(k,2) = spread(i+1);
            trade(k,3) = 1;
            trade(k,4) = -beta;
            trade(k,5) = testPrices(i+1,1);
            trade(k,6) = testPrices(i+1,2);
            trade(k,7) = 0;
            k = k + 1;
        end
    end
    if exist('trade', 'var')
        tradesNumber = size(trade,1);
        profit(h,3) = tradesNumber;
        profit(h,4) = (tradesNumber - 1) * 2 * g;
    else
        profit(h,3) = 0;
        profit(h,4) = 0;
    end
end 

В таблице ниже показаны проценты и исходы для пары акций с тикерами (VSYDP,NKHP).
Процент $G$ Трейды Прибыль
5 160,2224 7 1922,7
10 320,4449 5 2563,6
15 480,6673 5 3845,3
20 640,8898 5 5127,1
25 801,1122 5 6408,9
30 961,3347 5 7690,7
35 1121,6 5 8972,5
40 1281,8 3 5127,1
45 1442 3 5768
50 1602,2 3 6408,9
55 1762,4 3 7049,8
60 1922,7 3 7690,7
65 2082,9 3 8331,6
70 2243,1 3 8972,5
75 2403,3 3 9613,3
80 2563,6 3 10254
85 2723,8 1 0
90 2884 0 0

Чтобы определить значение $G$, мы просто выбираем то значение, которое даёт наибольшую прибыль, основанную на «истории».

[M, I] = max(profit(:,4));
bestG = profit(I,2);

Однако, хотя $G=2563,6$ (при 80% от $m$) даёт наибольшую прибыль, на практике мы не выбираем $G$ больше, чем $0,5m$, из-за трудностей, связанных с дальнейшим извлечением прибыли, поэтому возьмём $G=1121,6$ (при 35% от $m$).

Тестирование стратегии


После определения $G$ торговая стратегия применяется ко второй половине наблюдений.

clear strategy
for i = half + 1:T
    if abs(spread(i)) >= bestG
        strategy(1,1) = i;
        strategy(1,2) = spread(i);
        if spread(i) > 0
            strategy(1,3) = -1;
            strategy(1,4) = beta;
        else
            strategy(1,3) = 1;
            strategy(1,4) = -beta;
        end
        strategy(1,5) = testPrices(i,1);
        strategy(1,6) = testPrices(i,2);
        strategy(1,7) = 0;
        startIndex = i;
        break
    end
end
if exist('strategy', 'var')
    k = 2;
    for i = startIndex:T-1
        if (strategy(k-1,3) ~= 0)
            if (spread(i) >= 0) && (spread(i+1) <= 0)
                strategy(k,1) = i+1;
                strategy(k,2) = spread(i+1);
                strategy(k,3) = 0;
                strategy(k,4) = 0;
                strategy(k,5) = testPrices(i+1,1);
                strategy(k,6) = testPrices(i+1,2);
                strategy(k,7) = strategy(k-1,2) - spread(i+1);
                k = k + 1;
            end
            if (spread(i) <= 0) && (spread(i+1) >= 0)
                strategy(k,1) = i+1;
                strategy(k,2) = spread(i+1);
                strategy(k,3) = 0;
                strategy(k,4) = 0;
                strategy(k,5) = testPrices(i+1,1);
                strategy(k,6) = testPrices(i+1,2);
                strategy(k,7) = spread(i+1) - strategy(k-1,2);
                k = k + 1;
            end
        else
            if (spread(i) <= bestG) && (spread(i+1) >= bestG)
                strategy(k,1) = i+1;
                strategy(k,2) = spread(i+1);
                strategy(k,3) = -1;
                strategy(k,4) = beta;
                strategy(k,5) = testPrices(i+1,1);
                strategy(k,6) = testPrices(i+1,2);
                strategy(k,7) = 0;
                k = k + 1;
            end
            if (spread(i) >= -bestG) && (spread(i+1) <= -bestG)
                strategy(k,1) = i+1;
                strategy(k,2) = spread(i+1);
                strategy(k,3) = 1;
                strategy(k,4) = -beta;
                strategy(k,5) = testPrices(i+1,1);
                strategy(k,6) = testPrices(i+1,2);
                strategy(k,7) = 0;
                k = k + 1;
            end
        end
    end
end
if exist('strategy', 'var')
    totalProfit = sum(strategy(:,7));
else
    totalProfit = 0;
end

При $G=1121,6$ прибыль извлекается 3 раза. Другими словами, разность 3 раза перемещается от 0 до $G$ и обратно. Обратите внимание, что реализация такой стратегии включает 6 трейдов, так как для того, чтобы встать в позицию и выйти из неё, требуется совершить два трейда.

На рисунке и в таблице ниже показаны все 6 моментов торговли.

Трейд $t$ $s_t$ Позиция $(Y,X)$ Цена $Y$ Цена $X$ Прибыль
1 108 3145,9 (-1; +35,6527) 13200 282 -
2 128 -211,447 Ликвидация 9700 278 3357,4
3 134 -1161,9 (+1; -35,6527) 8500 271 -
4 171 14,6605 Ликвидация 8500 238 1176,5
5 205 -1184,5 (+1; -35,6527) 7800 252 -
6 212 185,1035 Ликвидация 8100 222 1369,6
  Итого 5903,5

Прибыль, полученная здесь, составляет не менее $3G = 3 \cdot 1121,6 = 3364,8$. Здесь используются цены закрытия вместо данных внутри дня, поэтому мы не торгуем в точках $-G$, 0 и $G$. Как видно из таблицы, доходность за 107 торговых дней составила 25,39% без учёта комиссий, объёма и пр. То есть это примерно 60,74% годовых по очень грубым прикидкам.

Также были проведены тесты для 58 пар на NYSE. Там, если не считать нулевой прибыли, приблизительная годовая доходность при данной стратегии колебалась от 23,34% до 208% без учёта комиссий, объёма и пр.

Тестирование альтернативной стратегии


Вместо того, чтобы закрывать позицию, когда спред приближается к нулю, можно перевернуть позицию, когда спред достигает $G$ на противоположной стороне от нуля. Предположим, что мы продали $1Y$ и купили $35,6527X$, так как разность была больше $G$.

Теперь можно дождаться того момента, когда разность достигнет $-G$, купить $2Y$ и продать $2 \cdot 35,6527X$. В результате мы останемся с портфелем из длинной позиции размера $1Y$ и короткий позиции размера $35,6527X$.

clear strategyAlt
for i = half + 1:T
    if abs(spread(i)) >= bestG
        strategyAlt(1,1) = i;
        strategyAlt(1,2) = spread(i);
        if spread(i) > 0
            strategyAlt(1,3) = -1;
            strategyAlt(1,4) = beta;
        else
            strategyAlt(1,3) = 1;
            strategyAlt(1,4) = -beta;
        end
        strategyAlt(1,5) = testPrices(i,1);
        strategyAlt(1,6) = testPrices(i,2);
        strategyAlt(1,7) = 0;
        startIndex = i;
        break
    end
end
if exist('strategyAlt', 'var')
    d = 2;
    for i = startIndex:T-1
        if (strategyAlt(d-1,2) >= bestG) && (spread(i) >= -bestG) && (spread(i+1) <= -bestG)
            strategyAlt(d,1) = i+1;
            strategyAlt(d,2) = spread(i+1);
            strategyAlt(d,3) = 1;
            strategyAlt(d,4) = -beta;
            strategyAlt(d,5) = testPrices(i+1,1);
            strategyAlt(d,6) = testPrices(i+1,2);
            strategyAlt(d,7) = strategyAlt(d-1,2) - spread(i+1);
            d = d + 1;
        end
        if (strategyAlt(d-1,2) <= -bestG) && (spread(i) <= bestG) && (spread(i+1) >= bestG)
            strategyAlt(d,1) = i+1;
            strategyAlt(d,2) = spread(i+1);
            strategyAlt(d,3) = -1;
            strategyAlt(d,4) = beta;
            strategyAlt(d,5) = testPrices(i+1,1);
            strategyAlt(d,6) = testPrices(i+1,2);
            strategyAlt(d,7) = spread(i+1) - strategyAlt(d-1,2);
            d = d + 1;
        end
    end
end
if exist('strategyAlt', 'var')
    totalAltProfit = sum(strategyAlt(:,7));
else
    totalAltProfit = 0;
end

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

Трейд $t$ $s_t$ Позиция $(Y,X)$ Цена $Y$ Цена $X$ Прибыль
1 108 3145,9 (-1; +35,6527) 13200 282 -
2 134 -1161,9 (+1; -35,6527) 8500 271 4307,8
  Итого 4307,8

Обратите внимание, что прибыль от перевертывания позиции равна $2G = 2 \cdot 1121,6 = 2243,2$, поэтому общая прибыль в данном случае составляет не менее $1 \cdot 2G = 2243,2$. Как видно из таблицы, доходность за 107 торговых дней составила 18,53% без учёта комиссий, объёма и пр. То есть это примерно 44,32% годовых по очень грубым прикидкам.

Также были проведены тесты для 58 пар на NYSE. Там, если не считать нулевой прибыли, приблизительная годовая доходность при данной стратегии колебалась от 17,63% до 201,53% без учёта комиссий, объёма и пр.

Данное изменение в торговой стратегии уменьшает количество трейдов в среднем в 2 раза. При этом сокращаются торговые издержки. Если бы разность двигалась вверх и вниз около 0, альтернативная стратегия была бы более прибыльной. Однако в рассмотренном случае, когда у пары есть тенденция двигаться между 0 и $-G$, позиция вообще ни разу не переворачивается, тогда как основная стратегия создаёт и ликвидирует позицию снова и снова… и делает деньги.

Выводы


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

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

Что почитать по теме


Терри Дж. Уотшем, Кейт Паррамоу. Количественные методы в финансах / Пер. с англ. под ред. М.Р. Ефимовой. — М.: Финансы, ЮНИТИ, 1999. — 527 с.

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

Комментарии (11)


  1. charypopper
    31.01.2018 18:27

    Здравствуйте.
    1) Соотношение в позиции не меняется со временем, и как я понял из предыдущей статьи высчитывалось по данным за весь период, не получается ли так, что вы берете данные для модели, чтобы торговать в момент времени t в котором эти данные еще не сформированы?
    2) Если мы говорим про акции, то покупка возможна только целого числа актива. В ваших расчетах позиция выглядит так= (+1; -35,6527), но можно открыть либо -35 либо -36, и тогда вы будете торговать немного не по своей стратегии, а еще и активом которого больше, что отразится на прибыли и рисках.


    1. AdrenaLeen Автор
      31.01.2018 18:52

      1) Если вы про бету (коэффициент коинтеграции), то да, она высчитывалась по данным за весь период. Сейчас пересчитала за половину, она получилась равной 34,5806 (вместо 35,6527). На конечный результат по прибыли от двух стратегий в данном примере это никак не повлияло, однако в будущем для большей точности бэктеста, соглашусь с вами, стоит пересчитывать бету для половины наблюдений. Остальные данные, используемые для модели, я нахожу адекватными.

      2) Вы заблуждаетесь. На Московской бирже есть режим торгов неполными лотами:
      moex.com/s1433
      Аналогичные условия предоставляет и Нью-Йоркская биржа.


      1. charypopper
        31.01.2018 19:08

        Спасибо за ссылку и перерасчет. И статьи (эта и предыдущая) хорошие, спасибо за предоставленные результаты о проделанной работе.


      1. rumatavz
        31.01.2018 22:36

        Про Нью-Йорк ничего не скажу, а на Мосбирже это скорее всего не возможно на практике. Во первых по ссылке посмотрите на время торгов. Во вторых наличие режима не означает наличие ликвидности там. Скорее всего там пустые стаканы. А в третьих нужно узнать можно ли частнику получить туда доступ, по умолчанию дают доступ к основным торгам.


        1. AdrenaLeen Автор
          01.02.2018 00:02

          1) Время торгов видела.

          2) Насчёт ликвидности всё относительно. Глядя на Московскую Биржу можно сказать, что наличие биржи не означает наличие ликвидности там. Её и в режиме основных торгов может и не быть.

          3) Насчёт доступа… зависит от брокера. Мой брокер, например, такую возможность предоставляет автоматически. В Квике это можно посмотреть через пункт меню Система — Заказ данных — Поток котировок. Выбираете там «Неполные лоты», и можно торговать параллельно с основной сессией.


  1. win16
    31.01.2018 18:29

    Спасибо за статью. Было интересно читать. Но тут есть принципиальная ошибка: как известно, коинтегрированность акций — это понятие врЕменное, но вы проводите форвард тестирование на интервале, про который точно знаете, что там акции коинтегрированы. То есть заглядываете в будущее. Вот если бы взяли набор акций, которые коинтегрированы первые полгода (конечно в них бы попали VSYDP и NKHP, но и еще несколько других), а потом проверили систему на второй половине года, тогда результат был бы другим: VSYDP и NKHP дали бы прибыль, а остальные скорей всего нет.


    1. charypopper
      31.01.2018 18:37

      А ведь может получиться что за период [t1;t2] пара не коинтегрирована, а за [t1;t3] — да (t3 > t2) и наоборот.


      1. win16
        31.01.2018 18:42

        Тоже верно :-) Так что не факт, что VSYDP и NKHP вообще попали бы в выборку коинтегрированных.


    1. AdrenaLeen Автор
      31.01.2018 18:58

      В режиме реального времени наличие коинтеграции проверяется каждый день. Соответственно, если в какой-то день пара перестаёт быть коинтегрированной, позиция закрывается. Это просто нужно учесть в алгоритме бэктестинга, если не хочется «заглядывать в будущее».


      1. win16
        31.01.2018 19:01

        Но ведь коинтегрированность не может в один день просто исчезнуть? Она же считается на каком-то периоде и затухать будет тоже постепенно. То есть будет запаздывание (как у скользящих средних)


        1. AdrenaLeen Автор
          31.01.2018 19:20

          Почему нет? Может. Коинтеграция — это, по сути, зависимость одного нестационарного временного ряда от другого нестационарного временного ряда. На каком-то периоде времени она может наблюдаться, при увеличении этого периода она может исчезать.

          И там даже дело может быть не только в зависимости, а ещё и в интегрируемости самих временных рядов, то есть является ли ряд нестационарным со стационарными приращениями, I(1). Это свойство, о котором я говорила в статье про стационарность, со временем может теряться у той или иной акции в паре.