Strasmore Research

Парний трейдинг і коінтеграція: пояснення

Коінтеграція потрібна для парного трейдингу, а кореляція не є таким тестом. Розрахуйте hedge ratio, spread і z score на реальних цінах закриття.

Pairs trading і cointegration тісно пов’язані: стратегія ґрунтується на розриві між цінами двох акцій, який знову повертається до стабільного рівня. Cointegration — це статистична властивість, яка підтверджує, що такий розрив узагалі існує. Correlation вимірює інше. Дві акції можуть мати високу кореляцію й водночас назавжди розійтися в ціні. А дві акції, чиї денні рухи майже не збігаються, можуть роками зберігати стабільний розрив. На цій сторінці вручну розраховано hedge ratio, spread і z score, виміряно всі три показники на реальних цінах закриття, а також приділено стільки ж уваги тому, як ця конструкція дає збій.

Коінтеграція в парному трейдингу

Кореляція показує, наскільки узгоджено змінюються денні відсоткові значення двох акцій. Вона варіюється від -1 до +1, де +1 означає, що акції зростали й падали разом на кожній сесії у вибірці. Кореляція нічого не говорить про відстань між двома цінами.

Коінтеграція описує саме цю відстань. Від ціни акції B віднімають фіксований множник, помножений на ціну акції A, і аналізують залишок. Якщо цей ряд, тобто spread, залишається в обмеженому діапазоні, а не віддаляється безмежно, дві акції є коінтегрованими. Самі ціни можуть рухатися куди завгодно. Їхня комбінація — ні.

У підручниках це часто зображають як собаку на повідку. Власник і собака рухаються вулицею непередбачуваними траєкторіями, але повідок утримує відстань між ними в певних межах. Кореляція запитує, чи роблять вони крок одночасно. Коінтеграція — чи утримує повідок.

Наведена нижче таблиця вимірює першу ідею для п’яти відомих пар у 2024 і 2025 роках. У стовпці return_corr наведено кореляцію денних відсоткових змін — саме це зазвичай мають на увазі під словом «кореляція». У стовпці price_corr наведено кореляцію фактичних цін закриття. Це значення завищує схожість майже будь-яких двох акцій, які обидві зростали.

ЗапитКореляція денних дохідностей проти кореляції рівнів цін, п’ять знайомих пар (2024–2025)
Точний SQL-код для кожного числа
WITH
    px AS (
        SELECT
            ticker,
            date,
            toFloat64(any(close)) AS close_px
        FROM global_markets.stocks_daily_aggs
        WHERE ticker IN ('KO', 'PEP', 'HD', 'LOW', 'XOM', 'CVX', 'V', 'MA', 'AAPL', 'MSFT')
          AND date BETWEEN '2024-01-01' AND '2025-12-31'
        GROUP BY ticker, date
    ),
    rets AS (
        SELECT
            ticker,
            date,
            close_px,
            close_px / lagInFrame(close_px) OVER (
                PARTITION BY ticker ORDER BY date
                ROWS BETWEEN 1 PRECEDING AND CURRENT ROW
            ) - 1 AS ret
        FROM px
    )
SELECT
    p.pair                                  AS pair,
    round(corr(a.ret, b.ret), 3)            AS return_corr,
    round(corr(a.close_px, b.close_px), 3)  AS price_corr
FROM
(
    SELECT
        tupleElement(t, 1) AS leg_a,
        tupleElement(t, 2) AS leg_b,
        tupleElement(t, 3) AS pair
    FROM
    (
        SELECT arrayJoin([
            ('KO',   'PEP',  'KO / PEP'),
            ('HD',   'LOW',  'HD / LOW'),
            ('XOM',  'CVX',  'XOM / CVX'),
            ('V',    'MA',   'V / MA'),
            ('AAPL', 'MSFT', 'AAPL / MSFT')
        ]) AS t
    )
) AS p
INNER JOIN rets AS a ON a.ticker = p.leg_a
INNER JOIN rets AS b ON b.ticker = p.leg_b AND b.date = a.date
WHERE isFinite(a.ret) AND isFinite(b.ret)
GROUP BY pair
ORDER BY return_corr DESC
Run this yourself

Якщо ранжувати пари за кореляцією денних змін, HD / LOW посідає перше місце в таблиці зі значенням 0.868, тоді як кореляція двох рівнів цін за той самий період становила 0.849. У нижньому рядку AAPL / MSFT має кореляцію дохідностей на рівні 0.479 порівняно з кореляцією цін 0.508. Жоден зі стовпців не відповідає на запитання, яке ставить трейдер у парній стратегії. Обидва вимірюють спільний рух, тоді як для угоди потрібен показник відстані.

Як розрахувати hedge ratio і spread?

П’ять послідовних кроків для двох списків цін закриття. Звичайного Python достатньо для всього розрахунку, а math — єдиний імпорт: без пакетів, постачальника даних і мережевого підключення.

  1. Центруйте кожен ряд. mean_a = sum(a) / len(a), і так само для списку b.
  2. Розрахуйте hedge ratio. Це нахил прямої, побудованої для B відносно A: beta = sum((a[i] - mean_a) * (b[i] - mean_b) for i in range(len(a))) / sum((x - mean_a) ** 2 for x in a). Показник означає кількість одиниць A, яка компенсує одну одиницю B.
  3. Побудуйте spread. spread = [b[i] - beta * a[i] for i in range(len(a))]. Для коінтегрованої пари цей ряд коливається навколо певного рівня, а не рухається в сталому тренді.
  4. Застосуйте ковзне вікно. За допомогою w = spread[i - win + 1 : i + 1] середнє вікна дорівнює mu = sum(w) / len(w), а вибіркове стандартне відхилення — sd = math.sqrt(sum((x - mu) ** 2 for x in w) / (len(w) - 1)). Ковзне вікно закінчується в день, для якого виконується оцінювання, і не використовує майбутні дані.
  5. Розрахуйте оцінку та виведіть результат. z = (spread[i] - mu) / sd, потім виведіть рядок для входу, коли abs(z) > 1.5, і рядок для виходу, коли abs(z) < 0.5. Значення z, що дорівнює 2, означає, що spread перебуває на два стандартні відхилення вище свого недавнього середнього.

Для перевірки достатньо двох жорстко заданих списків цін, а win, що дорівнює 5, робить цей навчальний приклад зрозумілим. У практичній роботі зазвичай використовують вікно приблизно на 63 торгові сесії, тобто близько кварталу. Пороги 1.5 для входу та 0.5 для виходу — це умовні значення, які переходять з одного навчального матеріалу до іншого. Вони не є результатом дослідження, і в наведених нижче панелях їх не розраховували.

Який вигляд має z-score парного трейдингу на реальних цінах?

Нижче наведено той самий розрахунок для двох великих виробників напоїв — KO і PEP. Коефіцієнт хеджування розраховано лише на основі цін закриття за 2023 рік, після чого його зафіксовано на весь період 2024–2025 років, який охоплює графік. Оцінювання нахилу на тій самій вибірці, яку надалі використовують для розрахунку z-score, є упередженням через використання майбутньої інформації під час backtesting, а розділення періоду оцінювання та періоду розрахунку показника є найпростішим захистом від нього.

ЗапитТижневий z-показник спреду KO/PEP, коефіцієнт хеджування розраховано лише за 2023 рік
Точний SQL-код для кожного числа
WITH
    daily AS (
        SELECT
            date,
            anyIf(toFloat64(close), ticker = 'PEP') AS pep,
            anyIf(toFloat64(close), ticker = 'KO')  AS ko
        FROM global_markets.stocks_daily_aggs
        WHERE ticker IN ('KO', 'PEP')
          AND date BETWEEN '2023-01-01' AND '2025-12-31'
        GROUP BY date
        HAVING pep > 0 AND ko > 0
    ),
    fitted AS (
        SELECT covarSamp(pep, ko) / varSamp(ko) AS beta
        FROM daily
        WHERE date < '2024-01-01'
    ),
    spread AS (
        SELECT
            daily.date                          AS date,
            daily.pep - fitted.beta * daily.ko  AS spread_usd
        FROM daily
        CROSS JOIN fitted
    ),
    scored AS (
        SELECT
            date,
            (spread_usd - avg(spread_usd) OVER (
                 ORDER BY date ROWS BETWEEN 62 PRECEDING AND CURRENT ROW))
            / stddevSampStable(spread_usd) OVER (
                 ORDER BY date ROWS BETWEEN 62 PRECEDING AND CURRENT ROW) AS z
        FROM spread
    )
SELECT
    toString(toMonday(date))   AS week,
    round(argMax(z, date), 2)  AS z_score
FROM scored
WHERE date >= '2024-01-01'
  AND isFinite(z)
GROUP BY week
ORDER BY week
Run this yourself

На графіку показано 105 тижневих значень. Період починається із z-score -1.53 і завершується на рівні -0.68. Важливо оцінювати форму руху, а не окрему точку. Пара, яка поводиться відповідно до припущень стратегії, перетинає порогове значення, протягом кількох тижнів повертається до нульового рівня, а потім знову перетинає поріг. Пара, яка перестала повертатися до середнього, закріплюється за межами порогового значення й залишається там. На графіку це виглядає як довгий горизонтальний відрізок далеко від середнього рівня.

Чи завжди cointegrated spread повертається до середнього?

Ні. Найнадійніший спосіб це побачити — виміряти, що відбувалося після кожної початкової точки. На панелі нижче кожну сесію з 2019 року до кінця 2025 року розподілено за z score цього дня. Потім наведено середній z score цього самого spread через двадцять сесій. Використано логарифм цінового співвідношення, а не розрахунковий spread. Тому жодна регресія, оцінена постфактум, не впливає на графік.

ЗапитПоложення спреду KO/PEP через двадцять сесій за початковим z-показником (2019–2025)
Точний SQL-код для кожного числа
WITH
    daily AS (
        SELECT
            date,
            anyIf(toFloat64(close), ticker = 'PEP') AS pep,
            anyIf(toFloat64(close), ticker = 'KO')  AS ko
        FROM global_markets.stocks_daily_aggs
        WHERE ticker IN ('KO', 'PEP')
          AND date BETWEEN '2018-01-01' AND '2025-12-31'
        GROUP BY date
        HAVING pep > 0 AND ko > 0
    ),
    spread AS (
        SELECT
            date,
            log(pep / ko) AS log_ratio
        FROM daily
    ),
    scored AS (
        SELECT
            date,
            (log_ratio - avg(log_ratio) OVER (
                 ORDER BY date ROWS BETWEEN 62 PRECEDING AND CURRENT ROW))
            / stddevSampStable(log_ratio) OVER (
                 ORDER BY date ROWS BETWEEN 62 PRECEDING AND CURRENT ROW) AS z
        FROM spread
    ),
    horizon AS (
        SELECT
            date,
            z,
            leadInFrame(z, 20) OVER (
                ORDER BY date ROWS BETWEEN CURRENT ROW AND 20 FOLLOWING
            ) AS z_fwd
        FROM scored
        WHERE isFinite(z)
    )
SELECT
    multiIf(z < -2, 'z below -2',
            z < -1, 'z -2 to -1',
            z <  0, 'z -1 to 0',
            z <  1, 'z 0 to 1',
            z <  2, 'z 1 to 2',
                    'z above 2')  AS z_bucket,
    round(avg(z), 2)              AS avg_z_start,
    round(avg(z_fwd), 2)          AS avg_z_20d_later,
    count()                       AS episode_count
FROM horizon
WHERE date >= '2019-01-01'
  AND date <= '2025-11-15'
  AND isFinite(z_fwd)
GROUP BY z_bucket
ORDER BY avg_z_start
Run this yourself

Сесії, що починалися в діапазоні z below -2, мали середній показник -2.42 того дня. Через двадцять сесій середній показник цього самого spread становив -0.84 за результатами 138 сесій. На іншому кінці діапазону кошик z above 2 починався на рівні 2.64, а через двадцять сесій показник становив 0.92. Крива, що з обох кінців наближається до нуля, відповідає характерній формі mean reversion.

Читайте графік разом із колонкою кількості спостережень. У крайніх діапазонах найменше сесій. Крім того, сесії всередині одного діапазону значною мірою перекриваються, тому більшість рядків формують кілька довгих періодів. Середнє значення не є гарантією результату для будь-якої окремої точки входу.

Чому pairs trading перестає працювати?

Hedge ratio не є сталою величиною. Перераховуйте її на кожен календарний рік — і вона змінюється.

ЗапитКоефіцієнт хеджування перераховано для кожного календарного року, дві галузеві пари
Точний SQL-код для кожного числа
WITH daily AS (
    SELECT
        date,
        anyIf(toFloat64(close), ticker = 'PEP') AS pep,
        anyIf(toFloat64(close), ticker = 'KO')  AS ko,
        anyIf(toFloat64(close), ticker = 'LOW') AS lowes,
        anyIf(toFloat64(close), ticker = 'HD')  AS hd
    FROM global_markets.stocks_daily_aggs
    WHERE ticker IN ('KO', 'PEP', 'HD', 'LOW')
      AND date BETWEEN '2019-01-01' AND '2025-12-31'
    GROUP BY date
    HAVING ko > 0 AND pep > 0 AND hd > 0 AND lowes > 0
)
SELECT
    toYear(date)                                  AS year,
    round(covarSamp(pep, ko) / varSamp(ko), 3)    AS pep_on_ko_beta,
    round(covarSamp(lowes, hd) / varSamp(hd), 3)  AS lowes_on_hd_beta
FROM daily
GROUP BY year
ORDER BY year
Run this yourself

Нахил PEP щодо KO, виміряний 2.338 за період 2019 і -0.543 за період 2025, а поруч для порівняння наведено другу пару з того самого сектора. Backtest, у якому один нахил розраховують за всю історію, а потім використовують із першого дня, підставляє до кожної ранньої угоди значення, якого на той момент ніхто не міг знати. Рішення просте й ефективне: перераховувати параметри на ковзному вікні та оцінювати кожен день за параметрами, доступними саме цього дня. Відтворюваний backtest дає змогу іншому аналітику перевірити дотримання цієї дисципліни.

Корпоративні дії можуть за одну ніч змінити одну з ніг пари. Спліт чотири до одного перетворює одну акцію на чотири й зменшує котирувальну ціну вчетверо. Через це спред, розрахований за нескоригованими цінами на дату спліту, стрибком переходить на рівень, за яким торгів не було. Злиття, виділення бізнесу та зміни в індексах створюють аналогічні, але менш помітні ефекти. Починайте з історії цін із поправкою на спліти і враховуйте, що тикер може бути переданий зовсім іншій компанії. Саме це пояснює, чому тикери пошкоджують набори даних.

Під час руху неможливо відразу відрізнити структурний злам від можливості. Найвище z-значення, яке пара коли-небудь показувала, може бути як найкращою точкою входу в її історії, так і першим тижнем після завершення взаємозв’язку. У день події обидва сценарії виглядають як надмірно розтягнутий спред. Розмір позиції компенсує те, чого не можуть показати самі статистичні показники: розмір позиції за критерієм Kelly і розмір позиції з урахуванням цільової волатильності — це два підходи до визначення частки рахунку, на яку може вплинути один спред. Максимальна просадка показує, як тривале перебування по неправильний бік від середнього впливає на криву капіталу.

Ковзне z-значення не є тестом на коінтеграцію. Існують формальні тести. Процедура Engle-Granger спочатку оцінює регресію, а потім перевіряє залишковий спред на стаціонарність. Тест Johansen розширює цю ідею на випадки, де задіяно більше ніж два інструменти. Z-значення припускає наявність властивості, яку перевіряють ці тести, і без проблем покаже велике число для спреду, що не має стабільного середнього. Трендовий ряд завжди сильно відхиляється від власного ковзного середнього.

Примітки щодо даних і того, чого немає в цих числах

Панель кореляції використовує денні ціни закриття з 2024-01-01 до 2025-12-31 і розраховує дохідність за послідовними цінами закриття, виключаючи першу сесію для кожного інструмента. На графіку z-значення hedge ratio оцінюється лише на цінах закриття за 2023 рік. Потім для 2024 і 2025 років розраховуються показники на основі середнього за 63 сесії та вибіркового стандартного відхилення, а значення фіксуються на останньому спостереженні кожного тижня. Панель mean reversion завантажує історію з 2018 року й показує дані з 2019 року, тому для кожного дня, що оцінюється, доступне повне попереднє ковзне вікно. Розрахунок припиняється в середині листопада 2025 року, щоб для кожного дня в межах вибірки залишалося двадцять наступних сесій.

Жодна з цих панелей не є стратегією. У розрахунках немає торгових витрат, bid-ask spread, комісії за позику для короткої ноги, дивідендів чи фінансування. Використовуються лише ціни закриття. Це означає, що угода, позначена за ціною закриття, фактично відкривається за ціною, за якою виконати її було б неможливо.

FAQ

У чому різниця між кореляцією та коінтеграцією?

Кореляція показує, чи рухаються дві акції в ті самі дні та в одному напрямку, за шкалою від -1 до +1. Коінтеграція показує, чи залишається відстань між їхніми цінами в стабільному діапазоні. Пара може мати високий показник за першим критерієм і не відповідати другому. Саме тому кореляційний відбір часто знаходить пари, які згодом розходяться.

Як розрахувати hedge ratio у парному трейдингу?

Потрібно апроксимувати пряму, де ціна однієї акції є функцією ціни іншої, і взяти її нахил. Цей нахил показує, скільки одиниць першої акції компенсує одну одиницю другої. Це змінний показник, тому в більшості реалізацій його перераховують на ковзному вікні, а не один раз за весь період спостереження.

Який z score використовують трейдери пар для відкриття та закриття позицій?

У навчальних прикладах позицію зазвичай відкривають, коли показник відхиляється від ковзного середнього на 1,5–2 стандартних відхилення, а закривають поблизу 0,5. Ці значення є усталеними правилами з попередніх матеріалів, а не виміряними оптимальними параметрами. Їх зміна впливає і на кількість угод, і на тривалість утримання кожної позиції.

Чи можуть дві акції бути коінтегрованими за низької кореляції?

Так. Кореляцію розраховують на основі денних змін, а коінтеграцію — на основі рівня розриву. Тому акції, щоденні рухи яких рідко збігаються, усе одно можуть зберігати стабільну довгострокову відстань між цінами. Можлива й протилежна ситуація: висока денна кореляція за одночасного розширення розриву з року в рік.

Чому коінтегрована пара перестає працювати?

Зв’язок, який утримує розрив у межах діапазону, може змінитися. Причиною можуть бути злиття, spin-off, зміна структури бізнесу однієї компанії або зміна індексу, що впливає на склад власників акцій. Статистичні показники за дні до такого розриву виглядають так само, як і перед звичайним поверненням до середнього. Тому розмір позиції потрібно визначати з урахуванням цього ризику, а не намагатися його передбачити.

Кожна панель містить точний SQL-запит, на якому вона побудована. Тому в ній видно всі вікна, період апроксимації та межі bucket. Щоб застосувати такий самий spread і z score до пари, за якою ви стежите, сформулюйте запит простою англійською мовою в терміналі Strasmore.

#pairs trading#cointegration#mean reversion#backtesting#statistics