מסחר בזוגות וקואינטגרציה: מדריך מעשי
גלו מדוע קואינטגרציה חיונית למסחר בזוגות וכיצד היא נבדלת ממתאם. המדריך מסביר כיצד לבנות ידנית יחס גידור, spread וציון z על בסיס מחירי סגירה אמיתיים בשוק ההון.
מסחר בזוגות וקואינטגרציה
מסחר בזוגות (Pairs trading) וקואינטגרציה (cointegration) שלובים זה בזה: העסקה נשענת על פער בין מחירי שתי מניות שנוטה לחזור לרמה יציבה, וקואינטגרציה היא התכונה הסטטיסטית המעידה על קיומו של פער כזה. מתאם (correlation) מודד משהו אחר. שתי מניות יכולות להציג מתאם גבוה ועדיין להתרחק זו מזו לצמיתות, בעוד ששתי מניות שהתנועות היומיות שלהן בקושי מסונכרנות יכולות לשמור על פער יציב במשך שנים. דף זה בונה ידנית את יחס הגידור (hedge ratio), את ה-spread ואת ה-z score, מודד את שלושתם על מחירי סגירה בפועל, ולאחר מכן מקדיש מקום שווה להסבר על האופן שבו המבנה הזה נכשל.
מהי קואינטגרציה (Cointegration) במסחר בזוגות?
מתאם (Correlation) הוא מדד לעוצמת הקשר בין השינויים היומיים באחוזים של שתי מניות. הערך נע בין מינוס אחד לפלוס אחד, כאשר פלוס אחד מציין שהמניות עלו וירדו יחד בכל יום מסחר בתקופה הנבדקת. מדד זה אינו מעיד דבר על המרחק בין מחירי שתי המניות.
קואינטגרציה היא הצהרה על אותו מרחק. ניקח את מניה B, נחסיר ממנה כפולה קבועה של מניה A, ונבחן מה נותר. אם סדרת הנתונים הנותרת, ה-spread, נשארת בתוך טווח חסום במקום לסטות ללא הגבלה, הרי ששני השמות נמצאים בקואינטגרציה. המחירים עצמם חופשיים לנוע לכל כיוון, אך השילוב ביניהם אינו כזה.
הדימוי המקובל בספרי הלימוד הוא של כלב המחובר לרצועה. ההולך והכלב נעים במסלולים בלתי צפויים ברחוב, אך הרצועה שומרת על מרחק חסום ביניהם. המתאם בוחן האם השניים צועדים באותו רגע; הקואינטגרציה בוחנת האם הרצועה מחזיקה מעמד.
הטבלה להלן מודדת את הרעיון הראשון עבור חמישה זוגות מוכרים לאורך שנת 2024 ושנת 2025. עמודת return_corr מציגה את המתאם של השינויים היומיים באחוזים, שזהו המובן המקובל של המונח בפי רוב האנשים. עמודת price_corr מציגה את המתאם של מחירי הסגירה הגולמיים, מספר שמחמיא כמעט לכל זוג מניות ששתיהן עלו.
ה-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בדירוג לפי מתאם השינויים היומיים, HD / LOW ניצבת בראש הטבלה עם 0.868, בעוד שהמתאם בין רמות המחירים של השתיים באותה תקופה עמד על 0.849. בשורה התחתונה, AAPL / MSFT נושאת מתאם תשואות של 0.479 לצד מתאם מחירים של 0.508. אף אחת מהעמודות אינה עונה על השאלה שסוחר זוגות שואל. שתיהן מודדות תנועה משותפת, בעוד שהמסחר דורש מדד של מרחק.
כיצד מחשבים את יחס הגידור ואת ה-spread?
חמישה שלבים, לפי סדר, על פני שתי רשימות של מחירי סגירה. Python בסיסית מכסה את כל התהליך עם math כייבוא היחיד: ללא חבילות, ללא ספק נתונים וללא רשת.
- מרכוז כל סדרה.
mean_a = sum(a) / len(a), ואותו הדבר עבור רשימהb. - התאמת יחס הגידור. זהו השיפוע של קו ישר של 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. - בניית ה-spread.
spread = [b[i] - beta * a[i] for i in range(len(a))]. בצמד בעל אינטגרציה משותפת (cointegrated), סדרה זו נעה סביב רמה מסוימת במקום להפגין מגמה. - שימוש בחלון נע. עם
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)). "נע" משמעו שהחלון מסתיים ביום שבו מתבצע הדירוג ואינו משתמש בנתונים שלאחריו. - דירוג והדפסה.
z = (spread[i] - mu) / sd, לאחר מכן הדפסת שורת כניסה כאשרabs(z) > 1.5ושורת יציאה ברגע ש-abs(z) < 0.5. ערך z של שתיים מציב את ה-spread שתי סטיות תקן מעל הממוצע האחרון שלו.
שתי רשימות מחירים מוטמעות בקוד מספיקות כדי לצפות בתהליך רץ, וערך win של חמש שומר על דוגמה פשוטה וקריאה. עבודה רצינית משתמשת בערך הקרוב לשישים ושלושה ימי מסחר, בערך רבעון. אותם ספים, אחד וחצי לכניסה וחצי ליציאה, הם מוסכמות העוברות ממדריך אחד למשנהו. הם אינם ממצאים, ושום דבר בלוחות שלמטה לא בחר בהם.
כיצד נראה ציון Z של מסחר בזוגות (pairs trading) על מחירי אמת?
להלן אותו חישוב על שתי מניות מוכרות מתחום המשקאות, KO ו-PEP. יחס הגידור (hedge ratio) נגזר משערי הסגירה של שנת 2023 בלבד, ולאחר מכן נותר קבוע לאורך 2024 ו-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 '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הגרף מציג 105 קריאות שבועיות. הוא פותח את החלון בציון Z של -1.53 ומסיים אותו ב--0.68. יש לקרוא את המגמה הכללית ולא להתמקד בנקודה בודדת. זוג מניות המתנהג כפי שהאסטרטגיה מניחה, חוצה את רף הסף שלו, מבלה מספר שבועות בחזרה לכיוון האפס, וחוצה אותו שוב. זוג שהפסיק לחזור לממוצע נתקע מעבר לסף ונשאר שם, מה שנראה בתרשים כקטע שטוח וארוך הרחק מהמרכז.
האם מרווח משולב (cointegrated spread) תמיד חוזר לממוצע?
לא, והדרך הכנה ביותר לבחון זאת היא למדוד מה קרה לאחר כל נקודת התחלה. הלוח להלן מחלק לקבוצות (buckets) כל יום מסחר משנת 2019 ועד סוף 2025 לפי ציון ה-z של אותו יום, ולאחר מכן מציג את ממוצע ציון ה-z של אותו מרווח כעבור עשרים ימי מסחר. החישוב משתמש בלוגריתם של יחס המחירים במקום במרווח מותאם, כך ששום רגרסיה שחושבה בדיעבד אינה משפיעה על התרשים.
ה-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ימי מסחר שהחלו בקבוצה z below -2 הציגו ממוצע של -2.42 באותו יום, ואותו מרווח הציג ממוצע של -0.84 כעבור עשרים ימי מסחר, על פני 138 ימי מסחר. בקצה השני, הקבוצה z above 2 החלה ב-2.64 ומדדה 0.92 כעבור עשרים ימים. עקומה שנמשכת לעבר האפס משני הקצוות היא הצורה שחזרה לממוצע מייצרת.
יש לקרוא זאת תוך התבוננות בעמודת הספירה. הקבוצות הקיצוניות מכילות את מספר ימי המסחר הקטן ביותר, וימים בתוך קבוצה חופפים במידה רבה, כך שמספר קטן של רצפים ארוכים מספק את רוב השורות. ממוצע אינו הבטחה לגבי כניסה בודדת כלשהי.
מדוע מסחר בזוגות (pairs trading) מפסיק לעבוד?
יחס הגידור אינו קבוע. התאמה מחדש שלו בכל שנה קלנדרית מראה שהוא משתנה.
ה-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שיפוע היחס בין PEP ל-KO נמדד 2.338 לאורך 2019 ו--0.543 לאורך 2025, כאשר זוג נוסף מאותו סקטור מוצג להשוואה. בחינה לאחור (backtest) המחשבת שיפוע אחד על פני כל ההיסטוריה וסוחרת לפיו מהיום הראשון, מעניקה לכל עסקה מוקדמת נתון שאף סוחר לא יכול היה להחזיק באותו זמן. הפתרון משעמם אך יעיל: בצעו התאמה מחדש בחלון זמן מתגלגל, ודרגו כל יום לפי הפרמטרים שהיו זמינים באותו יום. בחינה לאחור ניתנת לשחזור מאפשרת לאדם אחר לבדוק את המשמעת הזו.
אירועים תאגידיים משכתבים רגל אחת בן לילה. פיצול מניות של ארבע-לאחת הופך מניה אחת לארבע ומחלק את המחיר המצוטט בארבע, כך שמרווח (spread) המחושב סביב תאריך זה על בסיס מחירים לא מותאמים מזנק לרמה שמעולם לא נסחרה. מיזוגים, ספין-אופים ושינויים במדדים מבצעים גרסאות שקטות יותר של אותו תהליך. התחילו עם היסטוריית מחירים מותאמת לפיצולים, ושימו לב שסימול (ticker) יכול לעבור לחברה שונה לחלוטין, וזו הסיבה לכך שסימולי טיקר משבשים מערכי נתונים.
שבירה והזדמנות נראות זהות בזמן התרחשותן. ציון ה-z הרחב ביותר שזוג רשם אי פעם הוא או נקודת הכניסה הטובה ביותר שהציע, או השבוע הראשון של מערכת יחסים שהסתיימה. שניהם נראים כמו מרווח מתוח באותו יום. גודל הפוזיציה נושא בנטל שהסטטיסטיקה אינה יכולה לשאת: קביעת גודל פוזיציה לפי קריטריון קלי ו-קביעת גודל פוזיציה לפי מיקוד תנודתיות הם שני מודלים המגדירים כמה מהתיק מותר למרווח בודד להזיז, ו-ירידה מקסימלית מהשיא (maximum drawdown) מודדת מה עושה רצף ארוך בצד הלא נכון של הממוצע לעקומת ההון.
ציון z מתגלגל אינו מבחן קו-אינטגרציה. קיימים מבחנים פורמליים. הליך Engle-Granger מריץ רגרסיה ואז בודק את המרווח הנותר עבור סטציונריות, ומבחן Johansen מרחיב את הרעיון מעבר לשני שמות. ציון z מניח את התכונה שהמבחנים הללו בודקים, והוא יציג בשמחה מספר גבוה על מרווח ללא ממוצע יציב כלל, שכן סדרה בעלת מגמה תמיד נמצאת רחוק מהממוצע המתגלגל של עצמה.
הערות על הנתונים ומה מספרים אלו מחסירים
לוח המתאם משתמש בשערי סגירה יומיים מ-2024-01-01 ועד 2025-12-31 ומחשב תשואות משערי סגירה עוקבים, תוך השמטת יום המסחר הראשון של כל שם. עקומת ציון ה-z מתאימה את יחס הגידור לפי שערי סגירה של 2023 בלבד, ואז מדרגת את 2024 ו-2025 עם ממוצע מתגלגל של 63 ימי מסחר וסטיית תקן מדגמית, שנדגמו לפי הקריאה האחרונה של כל שבוע. לוח ההיפוך טוען היסטוריה מ-2018 ומדווח מ-2019 ואילך, כך שלכל יום מדורג יש חלון מתגלגל מלא מאחוריו, והוא נעצר באמצע נובמבר 2025 כך שלכל יום יש עשרים ימי מסחר לפניו בתוך המדגם.
אף אחד מלוחות אלו אינו אסטרטגיה. הם אינם כוללים עלויות מסחר, מרווחי קנייה-מכירה (bid-ask spread), דמי השאלה על הרגל הקצרה, דיבידנדים או מימון. מדובר בשערי סגירה בלבד, מה שאומר שכניסה המסומנת בשער סגירה היא כניסה שאף אחד לא יכול היה לבצע בפועל.
שאלות נפוצות
מה ההבדל בין מתאם (Correlation) לבין אינטגרציה משותפת (Cointegration)?
מתאם מודד האם שתי מניות נעות באותם ימים ובאותו כיוון, על סקאלה שבין מינוס אחד לפלוס אחד. אינטגרציה משותפת מודדת האם המרחק בין מחירי המניות נשמר בטווח יציב. זוג מניות יכול לקבל ציון גבוה במדד הראשון ולהיכשל בשני; זו הסיבה השכיחה לכך שסינון לפי מתאם מניב זוגות שמתרחקים זה מזה לאחר מכן.
כיצד מחשבים את יחס הגידור (Hedge Ratio) במסחר בזוגות?
מתאימים קו ישר למחיר מניה אחת מול השנייה ולוקחים את השיפוע. שיפוע זה הוא מספר היחידות של המניה הראשונה שמקזז יחידה אחת של המניה השנייה. מדובר בערך דינמי, ולכן רוב היישומים מחשבים אותו מחדש על פני חלון זמן נע, ולא פעם אחת על פני כל המדגם.
באילו ערכי Z-score משתמשים סוחרי זוגות לכניסה ויציאה?
דוגמאות לימודיות מציעות בדרך כלל כניסה סביב סטייה של אחת וחצי עד שתיים מהממוצע הנע, ויציאה קרוב לסטייה של חצי. מספרים אלו הם מוסכמות שנשתמרו ממאמרים מוקדמים ולא אופטימום שנמדד; שינוי שלהם משנה הן את מספר העסקאות והן את משך ההחזקה של כל אחת מהן.
האם שתי מניות יכולות להיות באינטגרציה משותפת עם מתאם נמוך?
כן. מתאם מחושב על שינויים יומיים, בעוד אינטגרציה משותפת מחושבת על רמת הפער. לכן, שני ניירות ערך שתנועותיהם היומיות כמעט ואינן מסונכרנות יכולים עדיין לשמור על מרחק יציב בטווח הארוך. המצב ההפוך מתקיים גם הוא: מתאם יומי גבוה לצד פער שמתרחב משנה לשנה.
מדוע זוג שנמצא באינטגרציה משותפת מפסיק לעבוד?
הקשר ששומר על הפער עשוי להשתנות: מיזוג, פיצול, שינוי בתמהיל העסקי של אחת החברות, או שינוי במדד שמשנה את זהות המחזיקים במניה. הנתונים הסטטיסטיים בימים שלפני שבירת הקשר נראים זהים לימים שלפני חזרה לממוצע רגילה; זהו החלק שבו יש לנהל את גודל הפוזיציה במקום לנסות לחזות את השבירה.
כל פאנל כאן מגיע עם שאילתת ה-SQL המדויקת שמתחתיו, כך שחלונות הזמן, תקופת ההתאמה וגבולות ה-bucket גלויים לעין. כדי להריץ את אותו ה-spread ואת אותו ה-Z-score על זוג שאתם עוקבים אחריו, שאלו את השאלה באנגלית פשוטה בטרמינל של Strasmore.