Correlation and Cointegration (A Light Treatment) TODO
Concept
The correlation coefficient standardizes the degree of linear co-movement between two variables to a value between -1 and 1; it does not imply causation and fails to capture nonlinear relationships properly. In time series, when two series each have their own trend, correlation and regression coefficients can come out large even with no real relationship — the spurious regression problem — so correlation between levels shouldn't be trusted at face value. Cointegration describes a relationship where each series is individually non-stationary (has a unit root), but some linear combination of the two is stationary and mean-reverting — a statement that a long-run equilibrium exists between the two series. The standard procedure is to test each series for a unit root, estimate the cointegrating relationship, then test whether its residuals are stationary; if cointegration holds, an error-correction model can describe how fast short-term deviations revert to equilibrium. In short: correlation is a statement about simultaneous movement, and cointegration is a statement about a long-run relationship.
Pairs trading, hedge-ratio sizing, and analyzing peg deviations for stablecoins or LSTs are all really asking whether the levels of two series stay tied together over the long run — which is cointegration's territory, not correlation's.
Code & Formula
# 상관관계와 공적분(가볍게) — 수준(level) 상관의 함정 vs 스프레드의 평균회귀
# 두 계열이 각자 추세를 가지면 아무 관계가 없어도 수준끼리는 강하게 "상관"돼 보인다(허위회귀).
import random
import statistics
random.seed(3)
N = 500
def corr(xs, ys):
mx, my = statistics.mean(xs), statistics.mean(ys)
cov = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
sx = (sum((x - mx) ** 2 for x in xs)) ** 0.5
sy = (sum((y - my) ** 2 for y in ys)) ** 0.5
return cov / (sx * sy)
# 1) 서로 무관한 두 랜덤워크(각자 추세만 가짐) — 진짜 관계는 없다
a_level = [100.0]
b_level = [50.0]
for _ in range(N):
a_level.append(a_level[-1] + random.gauss(0.15, 1.0)) # 독립적인 상승 추세(추세가 노이즈를 압도)
b_level.append(b_level[-1] + random.gauss(0.10, 1.0))
a_ret = [a_level[i] - a_level[i - 1] for i in range(1, len(a_level))]
b_ret = [b_level[i] - b_level[i - 1] for i in range(1, len(b_level))]
print(f"[무관한 두 랜덤워크] 수준(level) 상관 = {corr(a_level, b_level):.3f} (허위로 높게 나옴)")
print(f"[무관한 두 랜덤워크] 수익률(return) 상관 = {corr(a_ret, b_ret):.3f} (실제로는 0에 가까움)")
# 2) 공적분 관계: 두 계열은 각자 비정상(추세)이지만 스프레드는 평균회귀하도록 구성
x_level = [100.0]
for _ in range(N):
x_level.append(x_level[-1] + random.gauss(0.0, 1.0))
spread = [0.0]
for _ in range(N):
# 스프레드가 커질수록 되돌아오는 힘(AR(1), 계수<1 => 평균회귀) + 노이즈
spread.append(spread[-1] * 0.8 + random.gauss(0.0, 0.5))
y_level = [x_level[i] - spread[i] for i in range(N + 1)]
print(f"\n[공적분 쌍] 수준 상관 = {corr(x_level, y_level):.3f}")
print(f"[공적분 쌍] 스프레드 평균/표준편차 = {statistics.mean(spread):.3f} / {statistics.stdev(spread):.3f}")
print(f"[공적분 쌍] 스프레드가 [-3, 3] 범위 안에 머문 비율 = {sum(-3 <= s <= 3 for s in spread) / len(spread):.2%}")
print("-> 스프레드가 특정 범위를 벗어나지 않고 되돌아온다면 두 계열이 장기적으로 묶여있다는 신호(공적분).")
Exercise
Take price time series for two assets, compute the correlation of log-price levels and the correlation of log returns separately, compare how different the values are, and plot whether the spread between the two series mean-reverts.
Practical Connection
Prices of different markets covering the same event, or of complementary outcome tokens, should stay tied together in the long run — so if the spread does not mean-revert, that's grounds to suspect a liquidity shortfall or a difference in settlement terms.
If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/math-50-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.