Strasmore Research

Pairs Trading and Cointegration: Wetin E Mean

Cointegration na wetin pairs trading really need, correlation no be the same test. Build hedge ratio, spread and z score by hand with real closing prices.

Pairs trading and cointegration dey go together: the trade dey rest on gap between two stock prices wey dey always come back to stable level, while cointegration na statistical property wey show say that kind gap dey exist at all. Correlation dey measure different thing. Two stocks fit get high correlation and still drift comot from each other forever. And two stocks wey their daily moves hardly align fit maintain steady gap for years. This page go build the hedge ratio, the spread and the z score by hand. E go measure all three with real closing prices. Then e go give equal space to how the construction fit fail.

Wetin be cointegration for pairs trading?

Correlation na how tightly two stocks’ daily percentage moves dey follow each other. E dey range from -1 to +1. +1 mean say dem rise and fall together for every session inside the sample. E no talk anything about the distance between the two prices.

Cointegration na statement about that distance. Take stock B, subtract fixed multiple of stock A, then look wetin remain. If that remaining series, wey be the spread, stay inside bounded range instead of wandering far away, the two names get cointegration. The prices themselves fit go anywhere. But the combination no fit.

The picture wey textbooks dey use na dog wey dey on leash. The walker and the dog fit follow unpredictable paths down the street, but the leash keep the distance between dem bounded. Correlation dey ask whether the two move at the same time. Cointegration dey ask whether the leash hold.

The panel below measure the first idea across five familiar pairs for 2024 and 2025. The return_corr column na correlation of daily percentage moves, na wetin people usually mean by the word. The price_corr column na correlation of raw closing prices. That number dey make almost any two names wey both rise look more similar than dem really be.

QueryDaily-return correlation vs price-level correlation, five familiar pairs (2024-2025)
The exact SQL behind every number
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

When dem rank am by daily-move correlation, HD / LOW dey top the panel at 0.868. Meanwhile, correlation of the two price levels for the same window measure 0.849. For the bottom row, AAPL / MSFT get return correlation of 0.479 beside price correlation of 0.508. Neither column answer the question wey pairs trader dey ask. Both measure co-movement, but the trade need measure of distance.

How you dey calculate hedge ratio and spread?

Na five steps, in order, across two lists of closing prices. Plain Python fit handle everything with math as the only import: no packages, no data vendor, no network.

  1. Center each series. mean_a = sum(a) / len(a), and do the same for list b.
  2. Fit the hedge ratio. Na the slope of one straight line for B against 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). Read am as the number of units of A wey offset one unit of B.
  3. Build the spread. spread = [b[i] - beta * a[i] for i in range(len(a))]. For cointegrated pair, this series dey hover around one level instead of trending.
  4. Take a trailing window. With w = spread[i - win + 1 : i + 1], the window mean na mu = sum(w) / len(w) and the sample standard deviation na sd = math.sqrt(sum((x - mu) ** 2 for x in w) / (len(w) - 1)). Trailing mean say the window end on the day wey you dey score, and e no use anything after that day.
  5. Score and print. z = (spread[i] - mu) / sd, then print entry line while abs(z) > 1.5 and exit line once abs(z) < 0.5. Z of two mean say the spread dey two standard deviations above its own recent average.

Two hard-coded price lists enough to watch this run, and win of five keep the toy example easy to read. Serious work dey use something closer to 63 sessions, about one quarter. Those thresholds, 1.5 to enter and 0.5 to exit, na conventions wey one tutorial dey pass to another. Dem no be findings, and nothing for the panels below pick dem.

How pairs trading z score dey look for real prices?

Na the same calculation be this one for two popular beverage companies, KO and PEP. The hedge ratio come from 2023 closing prices only. E remain fixed throughout 2024 and 2025, wey be the period wey chart cover. If you fit the slope with the same sample wey you later use to calculate the score, na look-ahead bias in backtesting be that. Keeping the fit window separate from the scoring window na the cheapest way to defend against am.

QueryWeekly z score of the KO/PEP spread, hedge ratio fitted on 2023 only
The exact SQL behind every number
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

The trace get 105 weekly readings. E start the window with z score of -1.53 and end am with -0.68. Focus on the shape, no be just one point. If pair dey behave as the strategy expect, e go cross the threshold, spend some weeks moving back toward zero, then cross again. If pair don stop mean reverting, e go stay beyond the threshold. For chart, this one go look like long flat stretch far from the middle.

Cointegrated spread dey always come back?

No. The honest way to see am na to measure wetin happen after every starting point. The panel below group each session from 2019 reach late 2025 according to that day’s z score. Then e report the average z score of the same spread twenty sessions later. E use the log of the price ratio instead of fitted spread, so no regression wey dem estimate after the fact touch the chart.

QueryWhere the KO/PEP spread dey twenty sessions later, by starting z score (2019-2025)
The exact SQL behind every number
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

Sessions wey start for the z below -2 bucket get average of -2.42 that day. The same spread get average of -0.84 twenty sessions later, across 138 sessions. For the other end, the z above 2 bucket start at 2.64 and measure 0.92 twenty sessions later. Curve wey dey pull toward zero from both ends na the shape mean reversion dey create.

Read am together with the count column. The buckets for the outer ends get the fewest sessions. Sessions inside one bucket overlap plenty, so small number of long stretches dey supply most of the rows. Average no be promise about any single entry.

Why pairs trading dey stop to work?

The hedge ratio no be constant. Refit am every calendar year and e dey move.

QueryHedge ratio refitted each calendar year, two sector pairs
The exact SQL behind every number
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

The slope of PEP on KO wey dem measure 2.338 across 2019 and -0.543 across 2025, with the second sector pair beside am for comparison. Backtest wey fit one slope across the full history and trade am from day one dey give every early trade one number wey nobody fit know at that time. The fix no exciting but e dey work: refit am with trailing window, then score each day with the parameters wey dey available that day. Reproducible backtest go make another person fit check that discipline.

Corporate actions fit rewrite one leg overnight. Four-for-one split turns one share to four and cuts the quoted price into one-quarter. Spread wey dem calculate across that date with unadjusted prices go jump to level wey never trade. Mergers, spin-offs and index changes fit do similar thing in quieter ways. Start with split-adjusted price history, and remember say one symbol fit later belong to completely different company. Na why ticker symbols break datasets.

Break and opportunity dey look the same while dem dey happen. The widest z score wey pair ever print fit be either the best entry e ever offer or the first week of relationship wey don end. Both go look like stretched spread that day. Position sizing carry the burden wey statistics no fit carry: Kelly criterion position sizing and volatility targeting position sizing na two frameworks for deciding how much of an account one spread fit move. Maximum drawdown measure wetin long period on the wrong side of the mean fit do to equity curve.

Rolling z score no be cointegration test. Formal tests dey. Engle-Granger procedure run the regression, then test the leftover spread for stationarity. Johansen test extend the idea beyond two names. Z score assume the property wey those tests dey check. E go still print big number for spread wey no get stable mean, because trending series always dey far from its own trailing average.

Data notes and wetin these numbers no include

The correlation panel use daily closes from 2024-01-01 through 2025-12-31 and calculate returns from consecutive closes, dropping each name first session. The z score trace fit the hedge ratio with 2023 closes only. Then e score 2024 and 2025 with trailing 63-session mean and sample standard deviation, sampled at the last reading of every week. The reversion panel load history from 2018 and report from 2019 onward. So every scored day get full trailing window behind am. E stop in mid-November 2025, so every day get twenty sessions ahead inside the sample.

None of these panels be strategy. Dem no include trading costs, bid-ask spread, borrow fee on the short leg, dividends or financing. Na closing prices only. This mean entry marked at a close na entry wey nobody fit fill at that price.

FAQ

Wetin be the difference between correlation and cointegration?

Correlation dey measure whether two stocks dey move on the same days and for the same direction, on a scale from -1 to +1. Cointegration dey measure whether the distance between their prices dey stay inside stable range. Pair fit score high for the first one and fail the second one. Na the usual reason correlation screen dey bring out pairs wey later drift apart.

How you dey calculate hedge ratio for pairs trading?

Fit straight line of one stock price against the other stock price, then use the slope. That slope na the number of units of the first stock wey offset one unit of the second. E dey change over time, so most implementations dey refit am with trailing window instead of fitting am once across the whole sample.

Which z score pairs traders dey use to enter and exit?

Tutorial examples normally enter around 1.5 to 2 standard deviations from the trailing mean, then exit near 0.5. Those numbers na conventions wey earlier write-ups pass down, no be measured optimum. If you change them, both the number of trades and how long each trade dey remain open go change.

Two stocks fit get cointegration with low correlation?

Yes. Correlation dey calculate from daily changes, while cointegration dey calculate from the level of the gap. So two stocks wey their day-to-day moves rarely line up fit still maintain stable long-run distance. The reverse fit happen too: high daily correlation alongside gap wey dey widen year after year.

Why cointegrated pair fit stop to work?

The link wey dey hold the gap together fit change. E fit be because of merger, spin-off, change for one company business mix, or index change wey alter who dey own the stock. The statistics for the days before a break fit look the same as the days before normal reversion. Na why position size need account for this risk instead of trying to predict am.

Every panel here dey come with the exact SQL underneath, so the windows, fit period and bucket edges all dey visible. To run the same spread and z score on pair wey you dey follow, ask the question for plain English on the Strasmore terminal.

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